165
js/components/playback.js
Normal file
165
js/components/playback.js
Normal file
@ -0,0 +1,165 @@
|
||||
/**
|
||||
* UI component that lets the use control auto-slide
|
||||
* playback via play/pause.
|
||||
*/
|
||||
export default class Playback {
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} container The component will append
|
||||
* itself to this
|
||||
* @param {function} progressCheck A method which will be
|
||||
* called frequently to get the current playback progress on
|
||||
* a range of 0-1
|
||||
*/
|
||||
constructor( container, progressCheck ) {
|
||||
|
||||
// Cosmetics
|
||||
this.diameter = 100;
|
||||
this.diameter2 = this.diameter/2;
|
||||
this.thickness = 6;
|
||||
|
||||
// Flags if we are currently playing
|
||||
this.playing = false;
|
||||
|
||||
// Current progress on a 0-1 range
|
||||
this.progress = 0;
|
||||
|
||||
// Used to loop the animation smoothly
|
||||
this.progressOffset = 1;
|
||||
|
||||
this.container = container;
|
||||
this.progressCheck = progressCheck;
|
||||
|
||||
this.canvas = document.createElement( 'canvas' );
|
||||
this.canvas.className = 'playback';
|
||||
this.canvas.width = this.diameter;
|
||||
this.canvas.height = this.diameter;
|
||||
this.canvas.style.width = this.diameter2 + 'px';
|
||||
this.canvas.style.height = this.diameter2 + 'px';
|
||||
this.context = this.canvas.getContext( '2d' );
|
||||
|
||||
this.container.appendChild( this.canvas );
|
||||
|
||||
this.render();
|
||||
|
||||
}
|
||||
|
||||
setPlaying( value ) {
|
||||
|
||||
const wasPlaying = this.playing;
|
||||
|
||||
this.playing = value;
|
||||
|
||||
// Start repainting if we weren't already
|
||||
if( !wasPlaying && this.playing ) {
|
||||
this.animate();
|
||||
}
|
||||
else {
|
||||
this.render();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
animate() {
|
||||
|
||||
const progressBefore = this.progress;
|
||||
|
||||
this.progress = this.progressCheck();
|
||||
|
||||
// When we loop, offset the progress so that it eases
|
||||
// smoothly rather than immediately resetting
|
||||
if( progressBefore > 0.8 && this.progress < 0.2 ) {
|
||||
this.progressOffset = this.progress;
|
||||
}
|
||||
|
||||
this.render();
|
||||
|
||||
if( this.playing ) {
|
||||
requestAnimationFrame( this.animate.bind( this ) );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the current progress and playback state.
|
||||
*/
|
||||
render() {
|
||||
|
||||
let progress = this.playing ? this.progress : 0,
|
||||
radius = ( this.diameter2 ) - this.thickness,
|
||||
x = this.diameter2,
|
||||
y = this.diameter2,
|
||||
iconSize = 28;
|
||||
|
||||
// Ease towards 1
|
||||
this.progressOffset += ( 1 - this.progressOffset ) * 0.1;
|
||||
|
||||
const endAngle = ( - Math.PI / 2 ) + ( progress * ( Math.PI * 2 ) );
|
||||
const startAngle = ( - Math.PI / 2 ) + ( this.progressOffset * ( Math.PI * 2 ) );
|
||||
|
||||
this.context.save();
|
||||
this.context.clearRect( 0, 0, this.diameter, this.diameter );
|
||||
|
||||
// Solid background color
|
||||
this.context.beginPath();
|
||||
this.context.arc( x, y, radius + 4, 0, Math.PI * 2, false );
|
||||
this.context.fillStyle = 'rgba( 0, 0, 0, 0.4 )';
|
||||
this.context.fill();
|
||||
|
||||
// Draw progress track
|
||||
this.context.beginPath();
|
||||
this.context.arc( x, y, radius, 0, Math.PI * 2, false );
|
||||
this.context.lineWidth = this.thickness;
|
||||
this.context.strokeStyle = 'rgba( 255, 255, 255, 0.2 )';
|
||||
this.context.stroke();
|
||||
|
||||
if( this.playing ) {
|
||||
// Draw progress on top of track
|
||||
this.context.beginPath();
|
||||
this.context.arc( x, y, radius, startAngle, endAngle, false );
|
||||
this.context.lineWidth = this.thickness;
|
||||
this.context.strokeStyle = '#fff';
|
||||
this.context.stroke();
|
||||
}
|
||||
|
||||
this.context.translate( x - ( iconSize / 2 ), y - ( iconSize / 2 ) );
|
||||
|
||||
// Draw play/pause icons
|
||||
if( this.playing ) {
|
||||
this.context.fillStyle = '#fff';
|
||||
this.context.fillRect( 0, 0, iconSize / 2 - 4, iconSize );
|
||||
this.context.fillRect( iconSize / 2 + 4, 0, iconSize / 2 - 4, iconSize );
|
||||
}
|
||||
else {
|
||||
this.context.beginPath();
|
||||
this.context.translate( 4, 0 );
|
||||
this.context.moveTo( 0, 0 );
|
||||
this.context.lineTo( iconSize - 4, iconSize / 2 );
|
||||
this.context.lineTo( 0, iconSize );
|
||||
this.context.fillStyle = '#fff';
|
||||
this.context.fill();
|
||||
}
|
||||
|
||||
this.context.restore();
|
||||
|
||||
}
|
||||
|
||||
on( type, listener ) {
|
||||
this.canvas.addEventListener( type, listener, false );
|
||||
}
|
||||
|
||||
off( type, listener ) {
|
||||
this.canvas.removeEventListener( type, listener, false );
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
this.playing = false;
|
||||
|
||||
if( this.canvas.parentNode ) {
|
||||
this.container.removeChild( this.canvas );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
300
js/config.js
Normal file
300
js/config.js
Normal file
@ -0,0 +1,300 @@
|
||||
/**
|
||||
* The default reveal.js config object.
|
||||
*/
|
||||
export default {
|
||||
|
||||
// The "normal" size of the presentation, aspect ratio will be preserved
|
||||
// when the presentation is scaled to fit different resolutions
|
||||
width: 960,
|
||||
height: 700,
|
||||
|
||||
// Factor of the display size that should remain empty around the content
|
||||
margin: 0.04,
|
||||
|
||||
// Bounds for smallest/largest possible scale to apply to content
|
||||
minScale: 0.2,
|
||||
maxScale: 2.0,
|
||||
|
||||
// Display presentation control arrows
|
||||
controls: true,
|
||||
|
||||
// Help the user learn the controls by providing hints, for example by
|
||||
// bouncing the down arrow when they first encounter a vertical slide
|
||||
controlsTutorial: true,
|
||||
|
||||
// Determines where controls appear, "edges" or "bottom-right"
|
||||
controlsLayout: 'bottom-right',
|
||||
|
||||
// Visibility rule for backwards navigation arrows; "faded", "hidden"
|
||||
// or "visible"
|
||||
controlsBackArrows: 'faded',
|
||||
|
||||
// Display a presentation progress bar
|
||||
progress: true,
|
||||
|
||||
// Display the page number of the current slide
|
||||
// - true: Show slide number
|
||||
// - false: Hide slide number
|
||||
//
|
||||
// Can optionally be set as a string that specifies the number formatting:
|
||||
// - "h.v": Horizontal . vertical slide number (default)
|
||||
// - "h/v": Horizontal / vertical slide number
|
||||
// - "c": Flattened slide number
|
||||
// - "c/t": Flattened slide number / total slides
|
||||
//
|
||||
// Alternatively, you can provide a function that returns the slide
|
||||
// number for the current slide. The function should take in a slide
|
||||
// object and return an array with one string [slideNumber] or
|
||||
// three strings [n1,delimiter,n2]. See #formatSlideNumber().
|
||||
slideNumber: false,
|
||||
|
||||
// Can be used to limit the contexts in which the slide number appears
|
||||
// - "all": Always show the slide number
|
||||
// - "print": Only when printing to PDF
|
||||
// - "speaker": Only in the speaker view
|
||||
showSlideNumber: 'all',
|
||||
|
||||
// Use 1 based indexing for # links to match slide number (default is zero
|
||||
// based)
|
||||
hashOneBasedIndex: false,
|
||||
|
||||
// Add the current slide number to the URL hash so that reloading the
|
||||
// page/copying the URL will return you to the same slide
|
||||
hash: false,
|
||||
|
||||
// Flags if we should monitor the hash and change slides accordingly
|
||||
respondToHashChanges: true,
|
||||
|
||||
// Enable support for jump-to-slide navigation shortcuts
|
||||
jumpToSlide: true,
|
||||
|
||||
// Push each slide change to the browser history. Implies `hash: true`
|
||||
history: false,
|
||||
|
||||
// Enable keyboard shortcuts for navigation
|
||||
keyboard: true,
|
||||
|
||||
// Optional function that blocks keyboard events when retuning false
|
||||
//
|
||||
// If you set this to 'focused', we will only capture keyboard events
|
||||
// for embedded decks when they are in focus
|
||||
keyboardCondition: null,
|
||||
|
||||
// Disables the default reveal.js slide layout (scaling and centering)
|
||||
// so that you can use custom CSS layout
|
||||
disableLayout: false,
|
||||
|
||||
// Enable the slide overview mode
|
||||
overview: true,
|
||||
|
||||
// Vertical centering of slides
|
||||
center: true,
|
||||
|
||||
// Enables touch navigation on devices with touch input
|
||||
touch: true,
|
||||
|
||||
// Loop the presentation
|
||||
loop: false,
|
||||
|
||||
// Change the presentation direction to be RTL
|
||||
rtl: false,
|
||||
|
||||
// Changes the behavior of our navigation directions.
|
||||
//
|
||||
// "default"
|
||||
// Left/right arrow keys step between horizontal slides, up/down
|
||||
// arrow keys step between vertical slides. Space key steps through
|
||||
// all slides (both horizontal and vertical).
|
||||
//
|
||||
// "linear"
|
||||
// Removes the up/down arrows. Left/right arrows step through all
|
||||
// slides (both horizontal and vertical).
|
||||
//
|
||||
// "grid"
|
||||
// When this is enabled, stepping left/right from a vertical stack
|
||||
// to an adjacent vertical stack will land you at the same vertical
|
||||
// index.
|
||||
//
|
||||
// Consider a deck with six slides ordered in two vertical stacks:
|
||||
// 1.1 2.1
|
||||
// 1.2 2.2
|
||||
// 1.3 2.3
|
||||
//
|
||||
// If you're on slide 1.3 and navigate right, you will normally move
|
||||
// from 1.3 -> 2.1. If "grid" is used, the same navigation takes you
|
||||
// from 1.3 -> 2.3.
|
||||
navigationMode: 'default',
|
||||
|
||||
// Randomizes the order of slides each time the presentation loads
|
||||
shuffle: false,
|
||||
|
||||
// Turns fragments on and off globally
|
||||
fragments: true,
|
||||
|
||||
// Flags whether to include the current fragment in the URL,
|
||||
// so that reloading brings you to the same fragment position
|
||||
fragmentInURL: true,
|
||||
|
||||
// Flags if the presentation is running in an embedded mode,
|
||||
// i.e. contained within a limited portion of the screen
|
||||
embedded: false,
|
||||
|
||||
// Flags if we should show a help overlay when the question-mark
|
||||
// key is pressed
|
||||
help: true,
|
||||
|
||||
// Flags if it should be possible to pause the presentation (blackout)
|
||||
pause: true,
|
||||
|
||||
// Flags if speaker notes should be visible to all viewers
|
||||
showNotes: false,
|
||||
|
||||
// Flags if slides with data-visibility="hidden" should be kep visible
|
||||
showHiddenSlides: false,
|
||||
|
||||
// Global override for autoplaying embedded media (video/audio/iframe)
|
||||
// - null: Media will only autoplay if data-autoplay is present
|
||||
// - true: All media will autoplay, regardless of individual setting
|
||||
// - false: No media will autoplay, regardless of individual setting
|
||||
autoPlayMedia: null,
|
||||
|
||||
// Global override for preloading lazy-loaded iframes
|
||||
// - null: Iframes with data-src AND data-preload will be loaded when within
|
||||
// the viewDistance, iframes with only data-src will be loaded when visible
|
||||
// - true: All iframes with data-src will be loaded when within the viewDistance
|
||||
// - false: All iframes with data-src will be loaded only when visible
|
||||
preloadIframes: null,
|
||||
|
||||
// Can be used to globally disable auto-animation
|
||||
autoAnimate: true,
|
||||
|
||||
// Optionally provide a custom element matcher that will be
|
||||
// used to dictate which elements we can animate between.
|
||||
autoAnimateMatcher: null,
|
||||
|
||||
// Default settings for our auto-animate transitions, can be
|
||||
// overridden per-slide or per-element via data arguments
|
||||
autoAnimateEasing: 'ease',
|
||||
autoAnimateDuration: 1.0,
|
||||
autoAnimateUnmatched: true,
|
||||
|
||||
// CSS properties that can be auto-animated. Position & scale
|
||||
// is matched separately so there's no need to include styles
|
||||
// like top/right/bottom/left, width/height or margin.
|
||||
autoAnimateStyles: [
|
||||
'opacity',
|
||||
'color',
|
||||
'background-color',
|
||||
'padding',
|
||||
'font-size',
|
||||
'line-height',
|
||||
'letter-spacing',
|
||||
'border-width',
|
||||
'border-color',
|
||||
'border-radius',
|
||||
'outline',
|
||||
'outline-offset'
|
||||
],
|
||||
|
||||
// Controls automatic progression to the next slide
|
||||
// - 0: Auto-sliding only happens if the data-autoslide HTML attribute
|
||||
// is present on the current slide or fragment
|
||||
// - 1+: All slides will progress automatically at the given interval
|
||||
// - false: No auto-sliding, even if data-autoslide is present
|
||||
autoSlide: 0,
|
||||
|
||||
// Stop auto-sliding after user input
|
||||
autoSlideStoppable: true,
|
||||
|
||||
// Use this method for navigation when auto-sliding (defaults to navigateNext)
|
||||
autoSlideMethod: null,
|
||||
|
||||
// Specify the average time in seconds that you think you will spend
|
||||
// presenting each slide. This is used to show a pacing timer in the
|
||||
// speaker view
|
||||
defaultTiming: null,
|
||||
|
||||
// Enable slide navigation via mouse wheel
|
||||
mouseWheel: false,
|
||||
|
||||
// Opens links in an iframe preview overlay
|
||||
// Add `data-preview-link` and `data-preview-link="false"` to customise each link
|
||||
// individually
|
||||
previewLinks: false,
|
||||
|
||||
// Exposes the reveal.js API through window.postMessage
|
||||
postMessage: true,
|
||||
|
||||
// Dispatches all reveal.js events to the parent window through postMessage
|
||||
postMessageEvents: false,
|
||||
|
||||
// Focuses body when page changes visibility to ensure keyboard shortcuts work
|
||||
focusBodyOnPageVisibilityChange: true,
|
||||
|
||||
// Transition style
|
||||
transition: 'slide', // none/fade/slide/convex/concave/zoom
|
||||
|
||||
// Transition speed
|
||||
transitionSpeed: 'default', // default/fast/slow
|
||||
|
||||
// Transition style for full page slide backgrounds
|
||||
backgroundTransition: 'fade', // none/fade/slide/convex/concave/zoom
|
||||
|
||||
// Parallax background image
|
||||
parallaxBackgroundImage: '', // CSS syntax, e.g. "a.jpg"
|
||||
|
||||
// Parallax background size
|
||||
parallaxBackgroundSize: '', // CSS syntax, e.g. "3000px 2000px"
|
||||
|
||||
// Parallax background repeat
|
||||
parallaxBackgroundRepeat: '', // repeat/repeat-x/repeat-y/no-repeat/initial/inherit
|
||||
|
||||
// Parallax background position
|
||||
parallaxBackgroundPosition: '', // CSS syntax, e.g. "top left"
|
||||
|
||||
// Amount of pixels to move the parallax background per slide step
|
||||
parallaxBackgroundHorizontal: null,
|
||||
parallaxBackgroundVertical: null,
|
||||
|
||||
// The maximum number of pages a single slide can expand onto when printing
|
||||
// to PDF, unlimited by default
|
||||
pdfMaxPagesPerSlide: Number.POSITIVE_INFINITY,
|
||||
|
||||
// Prints each fragment on a separate slide
|
||||
pdfSeparateFragments: true,
|
||||
|
||||
// Offset used to reduce the height of content within exported PDF pages.
|
||||
// This exists to account for environment differences based on how you
|
||||
// print to PDF. CLI printing options, like phantomjs and wkpdf, can end
|
||||
// on precisely the total height of the document whereas in-browser
|
||||
// printing has to end one pixel before.
|
||||
pdfPageHeightOffset: -1,
|
||||
|
||||
// Number of slides away from the current that are visible
|
||||
viewDistance: 3,
|
||||
|
||||
// Number of slides away from the current that are visible on mobile
|
||||
// devices. It is advisable to set this to a lower number than
|
||||
// viewDistance in order to save resources.
|
||||
mobileViewDistance: 2,
|
||||
|
||||
// The display mode that will be used to show slides
|
||||
display: 'block',
|
||||
|
||||
// Hide cursor if inactive
|
||||
hideInactiveCursor: true,
|
||||
|
||||
// Time before the cursor is hidden (in ms)
|
||||
hideCursorTime: 5000,
|
||||
|
||||
// Should we automatmically sort and set indices for fragments
|
||||
// at each sync? (See Reveal.sync)
|
||||
sortFragmentsOnSync: true,
|
||||
|
||||
// Script dependencies to load
|
||||
dependencies: [],
|
||||
|
||||
// Plugin objects to register and use for this presentation
|
||||
plugins: []
|
||||
|
||||
}
|
640
js/controllers/autoanimate.js
Normal file
640
js/controllers/autoanimate.js
Normal file
@ -0,0 +1,640 @@
|
||||
import { queryAll, extend, createStyleSheet, matches, closest } from '../utils/util.js'
|
||||
import { FRAGMENT_STYLE_REGEX } from '../utils/constants.js'
|
||||
|
||||
// Counter used to generate unique IDs for auto-animated elements
|
||||
let autoAnimateCounter = 0;
|
||||
|
||||
/**
|
||||
* Automatically animates matching elements across
|
||||
* slides with the [data-auto-animate] attribute.
|
||||
*/
|
||||
export default class AutoAnimate {
|
||||
|
||||
constructor( Reveal ) {
|
||||
|
||||
this.Reveal = Reveal;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs an auto-animation between the given slides.
|
||||
*
|
||||
* @param {HTMLElement} fromSlide
|
||||
* @param {HTMLElement} toSlide
|
||||
*/
|
||||
run( fromSlide, toSlide ) {
|
||||
|
||||
// Clean up after prior animations
|
||||
this.reset();
|
||||
|
||||
let allSlides = this.Reveal.getSlides();
|
||||
let toSlideIndex = allSlides.indexOf( toSlide );
|
||||
let fromSlideIndex = allSlides.indexOf( fromSlide );
|
||||
|
||||
// Ensure that both slides are auto-animate targets with the same data-auto-animate-id value
|
||||
// (including null if absent on both) and that data-auto-animate-restart isn't set on the
|
||||
// physically latter slide (independent of slide direction)
|
||||
if( fromSlide.hasAttribute( 'data-auto-animate' ) && toSlide.hasAttribute( 'data-auto-animate' )
|
||||
&& fromSlide.getAttribute( 'data-auto-animate-id' ) === toSlide.getAttribute( 'data-auto-animate-id' )
|
||||
&& !( toSlideIndex > fromSlideIndex ? toSlide : fromSlide ).hasAttribute( 'data-auto-animate-restart' ) ) {
|
||||
|
||||
// Create a new auto-animate sheet
|
||||
this.autoAnimateStyleSheet = this.autoAnimateStyleSheet || createStyleSheet();
|
||||
|
||||
let animationOptions = this.getAutoAnimateOptions( toSlide );
|
||||
|
||||
// Set our starting state
|
||||
fromSlide.dataset.autoAnimate = 'pending';
|
||||
toSlide.dataset.autoAnimate = 'pending';
|
||||
|
||||
// Flag the navigation direction, needed for fragment buildup
|
||||
animationOptions.slideDirection = toSlideIndex > fromSlideIndex ? 'forward' : 'backward';
|
||||
|
||||
// If the from-slide is hidden because it has moved outside
|
||||
// the view distance, we need to temporarily show it while
|
||||
// measuring
|
||||
let fromSlideIsHidden = fromSlide.style.display === 'none';
|
||||
if( fromSlideIsHidden ) fromSlide.style.display = this.Reveal.getConfig().display;
|
||||
|
||||
// Inject our auto-animate styles for this transition
|
||||
let css = this.getAutoAnimatableElements( fromSlide, toSlide ).map( elements => {
|
||||
return this.autoAnimateElements( elements.from, elements.to, elements.options || {}, animationOptions, autoAnimateCounter++ );
|
||||
} );
|
||||
|
||||
if( fromSlideIsHidden ) fromSlide.style.display = 'none';
|
||||
|
||||
// Animate unmatched elements, if enabled
|
||||
if( toSlide.dataset.autoAnimateUnmatched !== 'false' && this.Reveal.getConfig().autoAnimateUnmatched === true ) {
|
||||
|
||||
// Our default timings for unmatched elements
|
||||
let defaultUnmatchedDuration = animationOptions.duration * 0.8,
|
||||
defaultUnmatchedDelay = animationOptions.duration * 0.2;
|
||||
|
||||
this.getUnmatchedAutoAnimateElements( toSlide ).forEach( unmatchedElement => {
|
||||
|
||||
let unmatchedOptions = this.getAutoAnimateOptions( unmatchedElement, animationOptions );
|
||||
let id = 'unmatched';
|
||||
|
||||
// If there is a duration or delay set specifically for this
|
||||
// element our unmatched elements should adhere to those
|
||||
if( unmatchedOptions.duration !== animationOptions.duration || unmatchedOptions.delay !== animationOptions.delay ) {
|
||||
id = 'unmatched-' + autoAnimateCounter++;
|
||||
css.push( `[data-auto-animate="running"] [data-auto-animate-target="${id}"] { transition: opacity ${unmatchedOptions.duration}s ease ${unmatchedOptions.delay}s; }` );
|
||||
}
|
||||
|
||||
unmatchedElement.dataset.autoAnimateTarget = id;
|
||||
|
||||
}, this );
|
||||
|
||||
// Our default transition for unmatched elements
|
||||
css.push( `[data-auto-animate="running"] [data-auto-animate-target="unmatched"] { transition: opacity ${defaultUnmatchedDuration}s ease ${defaultUnmatchedDelay}s; }` );
|
||||
|
||||
}
|
||||
|
||||
// Setting the whole chunk of CSS at once is the most
|
||||
// efficient way to do this. Using sheet.insertRule
|
||||
// is multiple factors slower.
|
||||
this.autoAnimateStyleSheet.innerHTML = css.join( '' );
|
||||
|
||||
// Start the animation next cycle
|
||||
requestAnimationFrame( () => {
|
||||
if( this.autoAnimateStyleSheet ) {
|
||||
// This forces our newly injected styles to be applied in Firefox
|
||||
getComputedStyle( this.autoAnimateStyleSheet ).fontWeight;
|
||||
|
||||
toSlide.dataset.autoAnimate = 'running';
|
||||
}
|
||||
} );
|
||||
|
||||
this.Reveal.dispatchEvent({
|
||||
type: 'autoanimate',
|
||||
data: {
|
||||
fromSlide,
|
||||
toSlide,
|
||||
sheet: this.autoAnimateStyleSheet
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Rolls back all changes that we've made to the DOM so
|
||||
* that as part of animating.
|
||||
*/
|
||||
reset() {
|
||||
|
||||
// Reset slides
|
||||
queryAll( this.Reveal.getRevealElement(), '[data-auto-animate]:not([data-auto-animate=""])' ).forEach( element => {
|
||||
element.dataset.autoAnimate = '';
|
||||
} );
|
||||
|
||||
// Reset elements
|
||||
queryAll( this.Reveal.getRevealElement(), '[data-auto-animate-target]' ).forEach( element => {
|
||||
delete element.dataset.autoAnimateTarget;
|
||||
} );
|
||||
|
||||
// Remove the animation sheet
|
||||
if( this.autoAnimateStyleSheet && this.autoAnimateStyleSheet.parentNode ) {
|
||||
this.autoAnimateStyleSheet.parentNode.removeChild( this.autoAnimateStyleSheet );
|
||||
this.autoAnimateStyleSheet = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a FLIP animation where the `to` element starts out
|
||||
* in the `from` element position and animates to its original
|
||||
* state.
|
||||
*
|
||||
* @param {HTMLElement} from
|
||||
* @param {HTMLElement} to
|
||||
* @param {Object} elementOptions Options for this element pair
|
||||
* @param {Object} animationOptions Options set at the slide level
|
||||
* @param {String} id Unique ID that we can use to identify this
|
||||
* auto-animate element in the DOM
|
||||
*/
|
||||
autoAnimateElements( from, to, elementOptions, animationOptions, id ) {
|
||||
|
||||
// 'from' elements are given a data-auto-animate-target with no value,
|
||||
// 'to' elements are are given a data-auto-animate-target with an ID
|
||||
from.dataset.autoAnimateTarget = '';
|
||||
to.dataset.autoAnimateTarget = id;
|
||||
|
||||
// Each element may override any of the auto-animate options
|
||||
// like transition easing, duration and delay via data-attributes
|
||||
let options = this.getAutoAnimateOptions( to, animationOptions );
|
||||
|
||||
// If we're using a custom element matcher the element options
|
||||
// may contain additional transition overrides
|
||||
if( typeof elementOptions.delay !== 'undefined' ) options.delay = elementOptions.delay;
|
||||
if( typeof elementOptions.duration !== 'undefined' ) options.duration = elementOptions.duration;
|
||||
if( typeof elementOptions.easing !== 'undefined' ) options.easing = elementOptions.easing;
|
||||
|
||||
let fromProps = this.getAutoAnimatableProperties( 'from', from, elementOptions ),
|
||||
toProps = this.getAutoAnimatableProperties( 'to', to, elementOptions );
|
||||
|
||||
// Maintain fragment visibility for matching elements when
|
||||
// we're navigating forwards, this way the viewer won't need
|
||||
// to step through the same fragments twice
|
||||
if( to.classList.contains( 'fragment' ) ) {
|
||||
|
||||
// Don't auto-animate the opacity of fragments to avoid
|
||||
// conflicts with fragment animations
|
||||
delete toProps.styles['opacity'];
|
||||
|
||||
if( from.classList.contains( 'fragment' ) ) {
|
||||
|
||||
let fromFragmentStyle = ( from.className.match( FRAGMENT_STYLE_REGEX ) || [''] )[0];
|
||||
let toFragmentStyle = ( to.className.match( FRAGMENT_STYLE_REGEX ) || [''] )[0];
|
||||
|
||||
// Only skip the fragment if the fragment animation style
|
||||
// remains unchanged
|
||||
if( fromFragmentStyle === toFragmentStyle && animationOptions.slideDirection === 'forward' ) {
|
||||
to.classList.add( 'visible', 'disabled' );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// If translation and/or scaling are enabled, css transform
|
||||
// the 'to' element so that it matches the position and size
|
||||
// of the 'from' element
|
||||
if( elementOptions.translate !== false || elementOptions.scale !== false ) {
|
||||
|
||||
let presentationScale = this.Reveal.getScale();
|
||||
|
||||
let delta = {
|
||||
x: ( fromProps.x - toProps.x ) / presentationScale,
|
||||
y: ( fromProps.y - toProps.y ) / presentationScale,
|
||||
scaleX: fromProps.width / toProps.width,
|
||||
scaleY: fromProps.height / toProps.height
|
||||
};
|
||||
|
||||
// Limit decimal points to avoid 0.0001px blur and stutter
|
||||
delta.x = Math.round( delta.x * 1000 ) / 1000;
|
||||
delta.y = Math.round( delta.y * 1000 ) / 1000;
|
||||
delta.scaleX = Math.round( delta.scaleX * 1000 ) / 1000;
|
||||
delta.scaleX = Math.round( delta.scaleX * 1000 ) / 1000;
|
||||
|
||||
let translate = elementOptions.translate !== false && ( delta.x !== 0 || delta.y !== 0 ),
|
||||
scale = elementOptions.scale !== false && ( delta.scaleX !== 0 || delta.scaleY !== 0 );
|
||||
|
||||
// No need to transform if nothing's changed
|
||||
if( translate || scale ) {
|
||||
|
||||
let transform = [];
|
||||
|
||||
if( translate ) transform.push( `translate(${delta.x}px, ${delta.y}px)` );
|
||||
if( scale ) transform.push( `scale(${delta.scaleX}, ${delta.scaleY})` );
|
||||
|
||||
fromProps.styles['transform'] = transform.join( ' ' );
|
||||
fromProps.styles['transform-origin'] = 'top left';
|
||||
|
||||
toProps.styles['transform'] = 'none';
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Delete all unchanged 'to' styles
|
||||
for( let propertyName in toProps.styles ) {
|
||||
const toValue = toProps.styles[propertyName];
|
||||
const fromValue = fromProps.styles[propertyName];
|
||||
|
||||
if( toValue === fromValue ) {
|
||||
delete toProps.styles[propertyName];
|
||||
}
|
||||
else {
|
||||
// If these property values were set via a custom matcher providing
|
||||
// an explicit 'from' and/or 'to' value, we always inject those values.
|
||||
if( toValue.explicitValue === true ) {
|
||||
toProps.styles[propertyName] = toValue.value;
|
||||
}
|
||||
|
||||
if( fromValue.explicitValue === true ) {
|
||||
fromProps.styles[propertyName] = fromValue.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let css = '';
|
||||
|
||||
let toStyleProperties = Object.keys( toProps.styles );
|
||||
|
||||
// Only create animate this element IF at least one style
|
||||
// property has changed
|
||||
if( toStyleProperties.length > 0 ) {
|
||||
|
||||
// Instantly move to the 'from' state
|
||||
fromProps.styles['transition'] = 'none';
|
||||
|
||||
// Animate towards the 'to' state
|
||||
toProps.styles['transition'] = `all ${options.duration}s ${options.easing} ${options.delay}s`;
|
||||
toProps.styles['transition-property'] = toStyleProperties.join( ', ' );
|
||||
toProps.styles['will-change'] = toStyleProperties.join( ', ' );
|
||||
|
||||
// Build up our custom CSS. We need to override inline styles
|
||||
// so we need to make our styles vErY IMPORTANT!1!!
|
||||
let fromCSS = Object.keys( fromProps.styles ).map( propertyName => {
|
||||
return propertyName + ': ' + fromProps.styles[propertyName] + ' !important;';
|
||||
} ).join( '' );
|
||||
|
||||
let toCSS = Object.keys( toProps.styles ).map( propertyName => {
|
||||
return propertyName + ': ' + toProps.styles[propertyName] + ' !important;';
|
||||
} ).join( '' );
|
||||
|
||||
css = '[data-auto-animate-target="'+ id +'"] {'+ fromCSS +'}' +
|
||||
'[data-auto-animate="running"] [data-auto-animate-target="'+ id +'"] {'+ toCSS +'}';
|
||||
|
||||
}
|
||||
|
||||
return css;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the auto-animate options for the given element.
|
||||
*
|
||||
* @param {HTMLElement} element Element to pick up options
|
||||
* from, either a slide or an animation target
|
||||
* @param {Object} [inheritedOptions] Optional set of existing
|
||||
* options
|
||||
*/
|
||||
getAutoAnimateOptions( element, inheritedOptions ) {
|
||||
|
||||
let options = {
|
||||
easing: this.Reveal.getConfig().autoAnimateEasing,
|
||||
duration: this.Reveal.getConfig().autoAnimateDuration,
|
||||
delay: 0
|
||||
};
|
||||
|
||||
options = extend( options, inheritedOptions );
|
||||
|
||||
// Inherit options from parent elements
|
||||
if( element.parentNode ) {
|
||||
let autoAnimatedParent = closest( element.parentNode, '[data-auto-animate-target]' );
|
||||
if( autoAnimatedParent ) {
|
||||
options = this.getAutoAnimateOptions( autoAnimatedParent, options );
|
||||
}
|
||||
}
|
||||
|
||||
if( element.dataset.autoAnimateEasing ) {
|
||||
options.easing = element.dataset.autoAnimateEasing;
|
||||
}
|
||||
|
||||
if( element.dataset.autoAnimateDuration ) {
|
||||
options.duration = parseFloat( element.dataset.autoAnimateDuration );
|
||||
}
|
||||
|
||||
if( element.dataset.autoAnimateDelay ) {
|
||||
options.delay = parseFloat( element.dataset.autoAnimateDelay );
|
||||
}
|
||||
|
||||
return options;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object containing all of the properties
|
||||
* that can be auto-animated for the given element and
|
||||
* their current computed values.
|
||||
*
|
||||
* @param {String} direction 'from' or 'to'
|
||||
*/
|
||||
getAutoAnimatableProperties( direction, element, elementOptions ) {
|
||||
|
||||
let config = this.Reveal.getConfig();
|
||||
|
||||
let properties = { styles: [] };
|
||||
|
||||
// Position and size
|
||||
if( elementOptions.translate !== false || elementOptions.scale !== false ) {
|
||||
let bounds;
|
||||
|
||||
// Custom auto-animate may optionally return a custom tailored
|
||||
// measurement function
|
||||
if( typeof elementOptions.measure === 'function' ) {
|
||||
bounds = elementOptions.measure( element );
|
||||
}
|
||||
else {
|
||||
if( config.center ) {
|
||||
// More precise, but breaks when used in combination
|
||||
// with zoom for scaling the deck ¯\_(ツ)_/¯
|
||||
bounds = element.getBoundingClientRect();
|
||||
}
|
||||
else {
|
||||
let scale = this.Reveal.getScale();
|
||||
bounds = {
|
||||
x: element.offsetLeft * scale,
|
||||
y: element.offsetTop * scale,
|
||||
width: element.offsetWidth * scale,
|
||||
height: element.offsetHeight * scale
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
properties.x = bounds.x;
|
||||
properties.y = bounds.y;
|
||||
properties.width = bounds.width;
|
||||
properties.height = bounds.height;
|
||||
}
|
||||
|
||||
const computedStyles = getComputedStyle( element );
|
||||
|
||||
// CSS styles
|
||||
( elementOptions.styles || config.autoAnimateStyles ).forEach( style => {
|
||||
let value;
|
||||
|
||||
// `style` is either the property name directly, or an object
|
||||
// definition of a style property
|
||||
if( typeof style === 'string' ) style = { property: style };
|
||||
|
||||
if( typeof style.from !== 'undefined' && direction === 'from' ) {
|
||||
value = { value: style.from, explicitValue: true };
|
||||
}
|
||||
else if( typeof style.to !== 'undefined' && direction === 'to' ) {
|
||||
value = { value: style.to, explicitValue: true };
|
||||
}
|
||||
else {
|
||||
// Use a unitless value for line-height so that it inherits properly
|
||||
if( style.property === 'line-height' ) {
|
||||
value = parseFloat( computedStyles['line-height'] ) / parseFloat( computedStyles['font-size'] );
|
||||
}
|
||||
|
||||
if( isNaN(value) ) {
|
||||
value = computedStyles[style.property];
|
||||
}
|
||||
}
|
||||
|
||||
if( value !== '' ) {
|
||||
properties.styles[style.property] = value;
|
||||
}
|
||||
} );
|
||||
|
||||
return properties;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all element pairs that we can animate
|
||||
* between the given slides.
|
||||
*
|
||||
* @param {HTMLElement} fromSlide
|
||||
* @param {HTMLElement} toSlide
|
||||
*
|
||||
* @return {Array} Each value is an array where [0] is
|
||||
* the element we're animating from and [1] is the
|
||||
* element we're animating to
|
||||
*/
|
||||
getAutoAnimatableElements( fromSlide, toSlide ) {
|
||||
|
||||
let matcher = typeof this.Reveal.getConfig().autoAnimateMatcher === 'function' ? this.Reveal.getConfig().autoAnimateMatcher : this.getAutoAnimatePairs;
|
||||
|
||||
let pairs = matcher.call( this, fromSlide, toSlide );
|
||||
|
||||
let reserved = [];
|
||||
|
||||
// Remove duplicate pairs
|
||||
return pairs.filter( ( pair, index ) => {
|
||||
if( reserved.indexOf( pair.to ) === -1 ) {
|
||||
reserved.push( pair.to );
|
||||
return true;
|
||||
}
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies matching elements between slides.
|
||||
*
|
||||
* You can specify a custom matcher function by using
|
||||
* the `autoAnimateMatcher` config option.
|
||||
*/
|
||||
getAutoAnimatePairs( fromSlide, toSlide ) {
|
||||
|
||||
let pairs = [];
|
||||
|
||||
const codeNodes = 'pre';
|
||||
const textNodes = 'h1, h2, h3, h4, h5, h6, p, li';
|
||||
const mediaNodes = 'img, video, iframe';
|
||||
|
||||
// Explicit matches via data-id
|
||||
this.findAutoAnimateMatches( pairs, fromSlide, toSlide, '[data-id]', node => {
|
||||
return node.nodeName + ':::' + node.getAttribute( 'data-id' );
|
||||
} );
|
||||
|
||||
// Text
|
||||
this.findAutoAnimateMatches( pairs, fromSlide, toSlide, textNodes, node => {
|
||||
return node.nodeName + ':::' + node.innerText;
|
||||
} );
|
||||
|
||||
// Media
|
||||
this.findAutoAnimateMatches( pairs, fromSlide, toSlide, mediaNodes, node => {
|
||||
return node.nodeName + ':::' + ( node.getAttribute( 'src' ) || node.getAttribute( 'data-src' ) );
|
||||
} );
|
||||
|
||||
// Code
|
||||
this.findAutoAnimateMatches( pairs, fromSlide, toSlide, codeNodes, node => {
|
||||
return node.nodeName + ':::' + node.innerText;
|
||||
} );
|
||||
|
||||
pairs.forEach( pair => {
|
||||
// Disable scale transformations on text nodes, we transition
|
||||
// each individual text property instead
|
||||
if( matches( pair.from, textNodes ) ) {
|
||||
pair.options = { scale: false };
|
||||
}
|
||||
// Animate individual lines of code
|
||||
else if( matches( pair.from, codeNodes ) ) {
|
||||
|
||||
// Transition the code block's width and height instead of scaling
|
||||
// to prevent its content from being squished
|
||||
pair.options = { scale: false, styles: [ 'width', 'height' ] };
|
||||
|
||||
// Lines of code
|
||||
this.findAutoAnimateMatches( pairs, pair.from, pair.to, '.hljs .hljs-ln-code', node => {
|
||||
return node.textContent;
|
||||
}, {
|
||||
scale: false,
|
||||
styles: [],
|
||||
measure: this.getLocalBoundingBox.bind( this )
|
||||
} );
|
||||
|
||||
// Line numbers
|
||||
this.findAutoAnimateMatches( pairs, pair.from, pair.to, '.hljs .hljs-ln-line[data-line-number]', node => {
|
||||
return node.getAttribute( 'data-line-number' );
|
||||
}, {
|
||||
scale: false,
|
||||
styles: [ 'width' ],
|
||||
measure: this.getLocalBoundingBox.bind( this )
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
}, this );
|
||||
|
||||
return pairs;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method which returns a bounding box based on
|
||||
* the given elements offset coordinates.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* @return {Object} x, y, width, height
|
||||
*/
|
||||
getLocalBoundingBox( element ) {
|
||||
|
||||
const presentationScale = this.Reveal.getScale();
|
||||
|
||||
return {
|
||||
x: Math.round( ( element.offsetLeft * presentationScale ) * 100 ) / 100,
|
||||
y: Math.round( ( element.offsetTop * presentationScale ) * 100 ) / 100,
|
||||
width: Math.round( ( element.offsetWidth * presentationScale ) * 100 ) / 100,
|
||||
height: Math.round( ( element.offsetHeight * presentationScale ) * 100 ) / 100
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds matching elements between two slides.
|
||||
*
|
||||
* @param {Array} pairs List of pairs to push matches to
|
||||
* @param {HTMLElement} fromScope Scope within the from element exists
|
||||
* @param {HTMLElement} toScope Scope within the to element exists
|
||||
* @param {String} selector CSS selector of the element to match
|
||||
* @param {Function} serializer A function that accepts an element and returns
|
||||
* a stringified ID based on its contents
|
||||
* @param {Object} animationOptions Optional config options for this pair
|
||||
*/
|
||||
findAutoAnimateMatches( pairs, fromScope, toScope, selector, serializer, animationOptions ) {
|
||||
|
||||
let fromMatches = {};
|
||||
let toMatches = {};
|
||||
|
||||
[].slice.call( fromScope.querySelectorAll( selector ) ).forEach( ( element, i ) => {
|
||||
const key = serializer( element );
|
||||
if( typeof key === 'string' && key.length ) {
|
||||
fromMatches[key] = fromMatches[key] || [];
|
||||
fromMatches[key].push( element );
|
||||
}
|
||||
} );
|
||||
|
||||
[].slice.call( toScope.querySelectorAll( selector ) ).forEach( ( element, i ) => {
|
||||
const key = serializer( element );
|
||||
toMatches[key] = toMatches[key] || [];
|
||||
toMatches[key].push( element );
|
||||
|
||||
let fromElement;
|
||||
|
||||
// Retrieve the 'from' element
|
||||
if( fromMatches[key] ) {
|
||||
const primaryIndex = toMatches[key].length - 1;
|
||||
const secondaryIndex = fromMatches[key].length - 1;
|
||||
|
||||
// If there are multiple identical from elements, retrieve
|
||||
// the one at the same index as our to-element.
|
||||
if( fromMatches[key][ primaryIndex ] ) {
|
||||
fromElement = fromMatches[key][ primaryIndex ];
|
||||
fromMatches[key][ primaryIndex ] = null;
|
||||
}
|
||||
// If there are no matching from-elements at the same index,
|
||||
// use the last one.
|
||||
else if( fromMatches[key][ secondaryIndex ] ) {
|
||||
fromElement = fromMatches[key][ secondaryIndex ];
|
||||
fromMatches[key][ secondaryIndex ] = null;
|
||||
}
|
||||
}
|
||||
|
||||
// If we've got a matching pair, push it to the list of pairs
|
||||
if( fromElement ) {
|
||||
pairs.push({
|
||||
from: fromElement,
|
||||
to: element,
|
||||
options: animationOptions
|
||||
});
|
||||
}
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a all elements within the given scope that should
|
||||
* be considered unmatched in an auto-animate transition. If
|
||||
* fading of unmatched elements is turned on, these elements
|
||||
* will fade when going between auto-animate slides.
|
||||
*
|
||||
* Note that parents of auto-animate targets are NOT considered
|
||||
* unmatched since fading them would break the auto-animation.
|
||||
*
|
||||
* @param {HTMLElement} rootElement
|
||||
* @return {Array}
|
||||
*/
|
||||
getUnmatchedAutoAnimateElements( rootElement ) {
|
||||
|
||||
return [].slice.call( rootElement.children ).reduce( ( result, element ) => {
|
||||
|
||||
const containsAnimatedElements = element.querySelector( '[data-auto-animate-target]' );
|
||||
|
||||
// The element is unmatched if
|
||||
// - It is not an auto-animate target
|
||||
// - It does not contain any auto-animate targets
|
||||
if( !element.hasAttribute( 'data-auto-animate-target' ) && !containsAnimatedElements ) {
|
||||
result.push( element );
|
||||
}
|
||||
|
||||
if( element.querySelector( '[data-auto-animate-target]' ) ) {
|
||||
result = result.concat( this.getUnmatchedAutoAnimateElements( element ) );
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}, [] );
|
||||
|
||||
}
|
||||
|
||||
}
|
406
js/controllers/backgrounds.js
Normal file
406
js/controllers/backgrounds.js
Normal file
@ -0,0 +1,406 @@
|
||||
import { queryAll } from '../utils/util.js'
|
||||
import { colorToRgb, colorBrightness } from '../utils/color.js'
|
||||
|
||||
/**
|
||||
* Creates and updates slide backgrounds.
|
||||
*/
|
||||
export default class Backgrounds {
|
||||
|
||||
constructor( Reveal ) {
|
||||
|
||||
this.Reveal = Reveal;
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
this.element = document.createElement( 'div' );
|
||||
this.element.className = 'backgrounds';
|
||||
this.Reveal.getRevealElement().appendChild( this.element );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the slide background elements and appends them
|
||||
* to the background container. One element is created per
|
||||
* slide no matter if the given slide has visible background.
|
||||
*/
|
||||
create() {
|
||||
|
||||
// Clear prior backgrounds
|
||||
this.element.innerHTML = '';
|
||||
this.element.classList.add( 'no-transition' );
|
||||
|
||||
// Iterate over all horizontal slides
|
||||
this.Reveal.getHorizontalSlides().forEach( slideh => {
|
||||
|
||||
let backgroundStack = this.createBackground( slideh, this.element );
|
||||
|
||||
// Iterate over all vertical slides
|
||||
queryAll( slideh, 'section' ).forEach( slidev => {
|
||||
|
||||
this.createBackground( slidev, backgroundStack );
|
||||
|
||||
backgroundStack.classList.add( 'stack' );
|
||||
|
||||
} );
|
||||
|
||||
} );
|
||||
|
||||
// Add parallax background if specified
|
||||
if( this.Reveal.getConfig().parallaxBackgroundImage ) {
|
||||
|
||||
this.element.style.backgroundImage = 'url("' + this.Reveal.getConfig().parallaxBackgroundImage + '")';
|
||||
this.element.style.backgroundSize = this.Reveal.getConfig().parallaxBackgroundSize;
|
||||
this.element.style.backgroundRepeat = this.Reveal.getConfig().parallaxBackgroundRepeat;
|
||||
this.element.style.backgroundPosition = this.Reveal.getConfig().parallaxBackgroundPosition;
|
||||
|
||||
// Make sure the below properties are set on the element - these properties are
|
||||
// needed for proper transitions to be set on the element via CSS. To remove
|
||||
// annoying background slide-in effect when the presentation starts, apply
|
||||
// these properties after short time delay
|
||||
setTimeout( () => {
|
||||
this.Reveal.getRevealElement().classList.add( 'has-parallax-background' );
|
||||
}, 1 );
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
this.element.style.backgroundImage = '';
|
||||
this.Reveal.getRevealElement().classList.remove( 'has-parallax-background' );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a background for the given slide.
|
||||
*
|
||||
* @param {HTMLElement} slide
|
||||
* @param {HTMLElement} container The element that the background
|
||||
* should be appended to
|
||||
* @return {HTMLElement} New background div
|
||||
*/
|
||||
createBackground( slide, container ) {
|
||||
|
||||
// Main slide background element
|
||||
let element = document.createElement( 'div' );
|
||||
element.className = 'slide-background ' + slide.className.replace( /present|past|future/, '' );
|
||||
|
||||
// Inner background element that wraps images/videos/iframes
|
||||
let contentElement = document.createElement( 'div' );
|
||||
contentElement.className = 'slide-background-content';
|
||||
|
||||
element.appendChild( contentElement );
|
||||
container.appendChild( element );
|
||||
|
||||
slide.slideBackgroundElement = element;
|
||||
slide.slideBackgroundContentElement = contentElement;
|
||||
|
||||
// Syncs the background to reflect all current background settings
|
||||
this.sync( slide );
|
||||
|
||||
return element;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders all of the visual properties of a slide background
|
||||
* based on the various background attributes.
|
||||
*
|
||||
* @param {HTMLElement} slide
|
||||
*/
|
||||
sync( slide ) {
|
||||
|
||||
const element = slide.slideBackgroundElement,
|
||||
contentElement = slide.slideBackgroundContentElement;
|
||||
|
||||
const data = {
|
||||
background: slide.getAttribute( 'data-background' ),
|
||||
backgroundSize: slide.getAttribute( 'data-background-size' ),
|
||||
backgroundImage: slide.getAttribute( 'data-background-image' ),
|
||||
backgroundVideo: slide.getAttribute( 'data-background-video' ),
|
||||
backgroundIframe: slide.getAttribute( 'data-background-iframe' ),
|
||||
backgroundColor: slide.getAttribute( 'data-background-color' ),
|
||||
backgroundGradient: slide.getAttribute( 'data-background-gradient' ),
|
||||
backgroundRepeat: slide.getAttribute( 'data-background-repeat' ),
|
||||
backgroundPosition: slide.getAttribute( 'data-background-position' ),
|
||||
backgroundTransition: slide.getAttribute( 'data-background-transition' ),
|
||||
backgroundOpacity: slide.getAttribute( 'data-background-opacity' ),
|
||||
};
|
||||
|
||||
const dataPreload = slide.hasAttribute( 'data-preload' );
|
||||
|
||||
// Reset the prior background state in case this is not the
|
||||
// initial sync
|
||||
slide.classList.remove( 'has-dark-background' );
|
||||
slide.classList.remove( 'has-light-background' );
|
||||
|
||||
element.removeAttribute( 'data-loaded' );
|
||||
element.removeAttribute( 'data-background-hash' );
|
||||
element.removeAttribute( 'data-background-size' );
|
||||
element.removeAttribute( 'data-background-transition' );
|
||||
element.style.backgroundColor = '';
|
||||
|
||||
contentElement.style.backgroundSize = '';
|
||||
contentElement.style.backgroundRepeat = '';
|
||||
contentElement.style.backgroundPosition = '';
|
||||
contentElement.style.backgroundImage = '';
|
||||
contentElement.style.opacity = '';
|
||||
contentElement.innerHTML = '';
|
||||
|
||||
if( data.background ) {
|
||||
// Auto-wrap image urls in url(...)
|
||||
if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp|webp)([?#\s]|$)/gi.test( data.background ) ) {
|
||||
slide.setAttribute( 'data-background-image', data.background );
|
||||
}
|
||||
else {
|
||||
element.style.background = data.background;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a hash for this combination of background settings.
|
||||
// This is used to determine when two slide backgrounds are
|
||||
// the same.
|
||||
if( data.background || data.backgroundColor || data.backgroundGradient || data.backgroundImage || data.backgroundVideo || data.backgroundIframe ) {
|
||||
element.setAttribute( 'data-background-hash', data.background +
|
||||
data.backgroundSize +
|
||||
data.backgroundImage +
|
||||
data.backgroundVideo +
|
||||
data.backgroundIframe +
|
||||
data.backgroundColor +
|
||||
data.backgroundGradient +
|
||||
data.backgroundRepeat +
|
||||
data.backgroundPosition +
|
||||
data.backgroundTransition +
|
||||
data.backgroundOpacity );
|
||||
}
|
||||
|
||||
// Additional and optional background properties
|
||||
if( data.backgroundSize ) element.setAttribute( 'data-background-size', data.backgroundSize );
|
||||
if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor;
|
||||
if( data.backgroundGradient ) element.style.backgroundImage = data.backgroundGradient;
|
||||
if( data.backgroundTransition ) element.setAttribute( 'data-background-transition', data.backgroundTransition );
|
||||
|
||||
if( dataPreload ) element.setAttribute( 'data-preload', '' );
|
||||
|
||||
// Background image options are set on the content wrapper
|
||||
if( data.backgroundSize ) contentElement.style.backgroundSize = data.backgroundSize;
|
||||
if( data.backgroundRepeat ) contentElement.style.backgroundRepeat = data.backgroundRepeat;
|
||||
if( data.backgroundPosition ) contentElement.style.backgroundPosition = data.backgroundPosition;
|
||||
if( data.backgroundOpacity ) contentElement.style.opacity = data.backgroundOpacity;
|
||||
|
||||
// If this slide has a background color, we add a class that
|
||||
// signals if it is light or dark. If the slide has no background
|
||||
// color, no class will be added
|
||||
let contrastColor = data.backgroundColor;
|
||||
|
||||
// If no bg color was found, or it cannot be converted by colorToRgb, check the computed background
|
||||
if( !contrastColor || !colorToRgb( contrastColor ) ) {
|
||||
let computedBackgroundStyle = window.getComputedStyle( element );
|
||||
if( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) {
|
||||
contrastColor = computedBackgroundStyle.backgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
if( contrastColor ) {
|
||||
const rgb = colorToRgb( contrastColor );
|
||||
|
||||
// Ignore fully transparent backgrounds. Some browsers return
|
||||
// rgba(0,0,0,0) when reading the computed background color of
|
||||
// an element with no background
|
||||
if( rgb && rgb.a !== 0 ) {
|
||||
if( colorBrightness( contrastColor ) < 128 ) {
|
||||
slide.classList.add( 'has-dark-background' );
|
||||
}
|
||||
else {
|
||||
slide.classList.add( 'has-light-background' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the background elements to reflect the current
|
||||
* slide.
|
||||
*
|
||||
* @param {boolean} includeAll If true, the backgrounds of
|
||||
* all vertical slides (not just the present) will be updated.
|
||||
*/
|
||||
update( includeAll = false ) {
|
||||
|
||||
let currentSlide = this.Reveal.getCurrentSlide();
|
||||
let indices = this.Reveal.getIndices();
|
||||
|
||||
let currentBackground = null;
|
||||
|
||||
// Reverse past/future classes when in RTL mode
|
||||
let horizontalPast = this.Reveal.getConfig().rtl ? 'future' : 'past',
|
||||
horizontalFuture = this.Reveal.getConfig().rtl ? 'past' : 'future';
|
||||
|
||||
// Update the classes of all backgrounds to match the
|
||||
// states of their slides (past/present/future)
|
||||
Array.from( this.element.childNodes ).forEach( ( backgroundh, h ) => {
|
||||
|
||||
backgroundh.classList.remove( 'past', 'present', 'future' );
|
||||
|
||||
if( h < indices.h ) {
|
||||
backgroundh.classList.add( horizontalPast );
|
||||
}
|
||||
else if ( h > indices.h ) {
|
||||
backgroundh.classList.add( horizontalFuture );
|
||||
}
|
||||
else {
|
||||
backgroundh.classList.add( 'present' );
|
||||
|
||||
// Store a reference to the current background element
|
||||
currentBackground = backgroundh;
|
||||
}
|
||||
|
||||
if( includeAll || h === indices.h ) {
|
||||
queryAll( backgroundh, '.slide-background' ).forEach( ( backgroundv, v ) => {
|
||||
|
||||
backgroundv.classList.remove( 'past', 'present', 'future' );
|
||||
|
||||
if( v < indices.v ) {
|
||||
backgroundv.classList.add( 'past' );
|
||||
}
|
||||
else if ( v > indices.v ) {
|
||||
backgroundv.classList.add( 'future' );
|
||||
}
|
||||
else {
|
||||
backgroundv.classList.add( 'present' );
|
||||
|
||||
// Only if this is the present horizontal and vertical slide
|
||||
if( h === indices.h ) currentBackground = backgroundv;
|
||||
}
|
||||
|
||||
} );
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
// Stop content inside of previous backgrounds
|
||||
if( this.previousBackground ) {
|
||||
|
||||
this.Reveal.slideContent.stopEmbeddedContent( this.previousBackground, { unloadIframes: !this.Reveal.slideContent.shouldPreload( this.previousBackground ) } );
|
||||
|
||||
}
|
||||
|
||||
// Start content in the current background
|
||||
if( currentBackground ) {
|
||||
|
||||
this.Reveal.slideContent.startEmbeddedContent( currentBackground );
|
||||
|
||||
let currentBackgroundContent = currentBackground.querySelector( '.slide-background-content' );
|
||||
if( currentBackgroundContent ) {
|
||||
|
||||
let backgroundImageURL = currentBackgroundContent.style.backgroundImage || '';
|
||||
|
||||
// Restart GIFs (doesn't work in Firefox)
|
||||
if( /\.gif/i.test( backgroundImageURL ) ) {
|
||||
currentBackgroundContent.style.backgroundImage = '';
|
||||
window.getComputedStyle( currentBackgroundContent ).opacity;
|
||||
currentBackgroundContent.style.backgroundImage = backgroundImageURL;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Don't transition between identical backgrounds. This
|
||||
// prevents unwanted flicker.
|
||||
let previousBackgroundHash = this.previousBackground ? this.previousBackground.getAttribute( 'data-background-hash' ) : null;
|
||||
let currentBackgroundHash = currentBackground.getAttribute( 'data-background-hash' );
|
||||
if( currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== this.previousBackground ) {
|
||||
this.element.classList.add( 'no-transition' );
|
||||
}
|
||||
|
||||
this.previousBackground = currentBackground;
|
||||
|
||||
}
|
||||
|
||||
// If there's a background brightness flag for this slide,
|
||||
// bubble it to the .reveal container
|
||||
if( currentSlide ) {
|
||||
[ 'has-light-background', 'has-dark-background' ].forEach( classToBubble => {
|
||||
if( currentSlide.classList.contains( classToBubble ) ) {
|
||||
this.Reveal.getRevealElement().classList.add( classToBubble );
|
||||
}
|
||||
else {
|
||||
this.Reveal.getRevealElement().classList.remove( classToBubble );
|
||||
}
|
||||
}, this );
|
||||
}
|
||||
|
||||
// Allow the first background to apply without transition
|
||||
setTimeout( () => {
|
||||
this.element.classList.remove( 'no-transition' );
|
||||
}, 1 );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the position of the parallax background based
|
||||
* on the current slide index.
|
||||
*/
|
||||
updateParallax() {
|
||||
|
||||
let indices = this.Reveal.getIndices();
|
||||
|
||||
if( this.Reveal.getConfig().parallaxBackgroundImage ) {
|
||||
|
||||
let horizontalSlides = this.Reveal.getHorizontalSlides(),
|
||||
verticalSlides = this.Reveal.getVerticalSlides();
|
||||
|
||||
let backgroundSize = this.element.style.backgroundSize.split( ' ' ),
|
||||
backgroundWidth, backgroundHeight;
|
||||
|
||||
if( backgroundSize.length === 1 ) {
|
||||
backgroundWidth = backgroundHeight = parseInt( backgroundSize[0], 10 );
|
||||
}
|
||||
else {
|
||||
backgroundWidth = parseInt( backgroundSize[0], 10 );
|
||||
backgroundHeight = parseInt( backgroundSize[1], 10 );
|
||||
}
|
||||
|
||||
let slideWidth = this.element.offsetWidth,
|
||||
horizontalSlideCount = horizontalSlides.length,
|
||||
horizontalOffsetMultiplier,
|
||||
horizontalOffset;
|
||||
|
||||
if( typeof this.Reveal.getConfig().parallaxBackgroundHorizontal === 'number' ) {
|
||||
horizontalOffsetMultiplier = this.Reveal.getConfig().parallaxBackgroundHorizontal;
|
||||
}
|
||||
else {
|
||||
horizontalOffsetMultiplier = horizontalSlideCount > 1 ? ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) : 0;
|
||||
}
|
||||
|
||||
horizontalOffset = horizontalOffsetMultiplier * indices.h * -1;
|
||||
|
||||
let slideHeight = this.element.offsetHeight,
|
||||
verticalSlideCount = verticalSlides.length,
|
||||
verticalOffsetMultiplier,
|
||||
verticalOffset;
|
||||
|
||||
if( typeof this.Reveal.getConfig().parallaxBackgroundVertical === 'number' ) {
|
||||
verticalOffsetMultiplier = this.Reveal.getConfig().parallaxBackgroundVertical;
|
||||
}
|
||||
else {
|
||||
verticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 );
|
||||
}
|
||||
|
||||
verticalOffset = verticalSlideCount > 0 ? verticalOffsetMultiplier * indices.v : 0;
|
||||
|
||||
this.element.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px';
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
this.element.remove();
|
||||
|
||||
}
|
||||
|
||||
}
|
266
js/controllers/controls.js
vendored
Normal file
266
js/controllers/controls.js
vendored
Normal file
@ -0,0 +1,266 @@
|
||||
import { queryAll } from '../utils/util.js'
|
||||
import { isAndroid } from '../utils/device.js'
|
||||
|
||||
/**
|
||||
* Manages our presentation controls. This includes both
|
||||
* the built-in control arrows as well as event monitoring
|
||||
* of any elements within the presentation with either of the
|
||||
* following helper classes:
|
||||
* - .navigate-up
|
||||
* - .navigate-right
|
||||
* - .navigate-down
|
||||
* - .navigate-left
|
||||
* - .navigate-next
|
||||
* - .navigate-prev
|
||||
*/
|
||||
export default class Controls {
|
||||
|
||||
constructor( Reveal ) {
|
||||
|
||||
this.Reveal = Reveal;
|
||||
|
||||
this.onNavigateLeftClicked = this.onNavigateLeftClicked.bind( this );
|
||||
this.onNavigateRightClicked = this.onNavigateRightClicked.bind( this );
|
||||
this.onNavigateUpClicked = this.onNavigateUpClicked.bind( this );
|
||||
this.onNavigateDownClicked = this.onNavigateDownClicked.bind( this );
|
||||
this.onNavigatePrevClicked = this.onNavigatePrevClicked.bind( this );
|
||||
this.onNavigateNextClicked = this.onNavigateNextClicked.bind( this );
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const rtl = this.Reveal.getConfig().rtl;
|
||||
const revealElement = this.Reveal.getRevealElement();
|
||||
|
||||
this.element = document.createElement( 'aside' );
|
||||
this.element.className = 'controls';
|
||||
this.element.innerHTML =
|
||||
`<button class="navigate-left" aria-label="${ rtl ? 'next slide' : 'previous slide' }"><div class="controls-arrow"></div></button>
|
||||
<button class="navigate-right" aria-label="${ rtl ? 'previous slide' : 'next slide' }"><div class="controls-arrow"></div></button>
|
||||
<button class="navigate-up" aria-label="above slide"><div class="controls-arrow"></div></button>
|
||||
<button class="navigate-down" aria-label="below slide"><div class="controls-arrow"></div></button>`;
|
||||
|
||||
this.Reveal.getRevealElement().appendChild( this.element );
|
||||
|
||||
// There can be multiple instances of controls throughout the page
|
||||
this.controlsLeft = queryAll( revealElement, '.navigate-left' );
|
||||
this.controlsRight = queryAll( revealElement, '.navigate-right' );
|
||||
this.controlsUp = queryAll( revealElement, '.navigate-up' );
|
||||
this.controlsDown = queryAll( revealElement, '.navigate-down' );
|
||||
this.controlsPrev = queryAll( revealElement, '.navigate-prev' );
|
||||
this.controlsNext = queryAll( revealElement, '.navigate-next' );
|
||||
|
||||
// The left, right and down arrows in the standard reveal.js controls
|
||||
this.controlsRightArrow = this.element.querySelector( '.navigate-right' );
|
||||
this.controlsLeftArrow = this.element.querySelector( '.navigate-left' );
|
||||
this.controlsDownArrow = this.element.querySelector( '.navigate-down' );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the reveal.js config is updated.
|
||||
*/
|
||||
configure( config, oldConfig ) {
|
||||
|
||||
this.element.style.display = config.controls ? 'block' : 'none';
|
||||
|
||||
this.element.setAttribute( 'data-controls-layout', config.controlsLayout );
|
||||
this.element.setAttribute( 'data-controls-back-arrows', config.controlsBackArrows );
|
||||
|
||||
}
|
||||
|
||||
bind() {
|
||||
|
||||
// Listen to both touch and click events, in case the device
|
||||
// supports both
|
||||
let pointerEvents = [ 'touchstart', 'click' ];
|
||||
|
||||
// Only support touch for Android, fixes double navigations in
|
||||
// stock browser
|
||||
if( isAndroid ) {
|
||||
pointerEvents = [ 'touchstart' ];
|
||||
}
|
||||
|
||||
pointerEvents.forEach( eventName => {
|
||||
this.controlsLeft.forEach( el => el.addEventListener( eventName, this.onNavigateLeftClicked, false ) );
|
||||
this.controlsRight.forEach( el => el.addEventListener( eventName, this.onNavigateRightClicked, false ) );
|
||||
this.controlsUp.forEach( el => el.addEventListener( eventName, this.onNavigateUpClicked, false ) );
|
||||
this.controlsDown.forEach( el => el.addEventListener( eventName, this.onNavigateDownClicked, false ) );
|
||||
this.controlsPrev.forEach( el => el.addEventListener( eventName, this.onNavigatePrevClicked, false ) );
|
||||
this.controlsNext.forEach( el => el.addEventListener( eventName, this.onNavigateNextClicked, false ) );
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
unbind() {
|
||||
|
||||
[ 'touchstart', 'click' ].forEach( eventName => {
|
||||
this.controlsLeft.forEach( el => el.removeEventListener( eventName, this.onNavigateLeftClicked, false ) );
|
||||
this.controlsRight.forEach( el => el.removeEventListener( eventName, this.onNavigateRightClicked, false ) );
|
||||
this.controlsUp.forEach( el => el.removeEventListener( eventName, this.onNavigateUpClicked, false ) );
|
||||
this.controlsDown.forEach( el => el.removeEventListener( eventName, this.onNavigateDownClicked, false ) );
|
||||
this.controlsPrev.forEach( el => el.removeEventListener( eventName, this.onNavigatePrevClicked, false ) );
|
||||
this.controlsNext.forEach( el => el.removeEventListener( eventName, this.onNavigateNextClicked, false ) );
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the state of all control/navigation arrows.
|
||||
*/
|
||||
update() {
|
||||
|
||||
let routes = this.Reveal.availableRoutes();
|
||||
|
||||
// Remove the 'enabled' class from all directions
|
||||
[...this.controlsLeft, ...this.controlsRight, ...this.controlsUp, ...this.controlsDown, ...this.controlsPrev, ...this.controlsNext].forEach( node => {
|
||||
node.classList.remove( 'enabled', 'fragmented' );
|
||||
|
||||
// Set 'disabled' attribute on all directions
|
||||
node.setAttribute( 'disabled', 'disabled' );
|
||||
} );
|
||||
|
||||
// Add the 'enabled' class to the available routes; remove 'disabled' attribute to enable buttons
|
||||
if( routes.left ) this.controlsLeft.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
||||
if( routes.right ) this.controlsRight.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
||||
if( routes.up ) this.controlsUp.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
||||
if( routes.down ) this.controlsDown.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
||||
|
||||
// Prev/next buttons
|
||||
if( routes.left || routes.up ) this.controlsPrev.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
||||
if( routes.right || routes.down ) this.controlsNext.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
||||
|
||||
// Highlight fragment directions
|
||||
let currentSlide = this.Reveal.getCurrentSlide();
|
||||
if( currentSlide ) {
|
||||
|
||||
let fragmentsRoutes = this.Reveal.fragments.availableRoutes();
|
||||
|
||||
// Always apply fragment decorator to prev/next buttons
|
||||
if( fragmentsRoutes.prev ) this.controlsPrev.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
||||
if( fragmentsRoutes.next ) this.controlsNext.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
||||
|
||||
// Apply fragment decorators to directional buttons based on
|
||||
// what slide axis they are in
|
||||
if( this.Reveal.isVerticalSlide( currentSlide ) ) {
|
||||
if( fragmentsRoutes.prev ) this.controlsUp.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
||||
if( fragmentsRoutes.next ) this.controlsDown.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
||||
}
|
||||
else {
|
||||
if( fragmentsRoutes.prev ) this.controlsLeft.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
||||
if( fragmentsRoutes.next ) this.controlsRight.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if( this.Reveal.getConfig().controlsTutorial ) {
|
||||
|
||||
let indices = this.Reveal.getIndices();
|
||||
|
||||
// Highlight control arrows with an animation to ensure
|
||||
// that the viewer knows how to navigate
|
||||
if( !this.Reveal.hasNavigatedVertically() && routes.down ) {
|
||||
this.controlsDownArrow.classList.add( 'highlight' );
|
||||
}
|
||||
else {
|
||||
this.controlsDownArrow.classList.remove( 'highlight' );
|
||||
|
||||
if( this.Reveal.getConfig().rtl ) {
|
||||
|
||||
if( !this.Reveal.hasNavigatedHorizontally() && routes.left && indices.v === 0 ) {
|
||||
this.controlsLeftArrow.classList.add( 'highlight' );
|
||||
}
|
||||
else {
|
||||
this.controlsLeftArrow.classList.remove( 'highlight' );
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if( !this.Reveal.hasNavigatedHorizontally() && routes.right && indices.v === 0 ) {
|
||||
this.controlsRightArrow.classList.add( 'highlight' );
|
||||
}
|
||||
else {
|
||||
this.controlsRightArrow.classList.remove( 'highlight' );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
this.unbind();
|
||||
this.element.remove();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handlers for navigation control buttons.
|
||||
*/
|
||||
onNavigateLeftClicked( event ) {
|
||||
|
||||
event.preventDefault();
|
||||
this.Reveal.onUserInput();
|
||||
|
||||
if( this.Reveal.getConfig().navigationMode === 'linear' ) {
|
||||
this.Reveal.prev();
|
||||
}
|
||||
else {
|
||||
this.Reveal.left();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onNavigateRightClicked( event ) {
|
||||
|
||||
event.preventDefault();
|
||||
this.Reveal.onUserInput();
|
||||
|
||||
if( this.Reveal.getConfig().navigationMode === 'linear' ) {
|
||||
this.Reveal.next();
|
||||
}
|
||||
else {
|
||||
this.Reveal.right();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onNavigateUpClicked( event ) {
|
||||
|
||||
event.preventDefault();
|
||||
this.Reveal.onUserInput();
|
||||
|
||||
this.Reveal.up();
|
||||
|
||||
}
|
||||
|
||||
onNavigateDownClicked( event ) {
|
||||
|
||||
event.preventDefault();
|
||||
this.Reveal.onUserInput();
|
||||
|
||||
this.Reveal.down();
|
||||
|
||||
}
|
||||
|
||||
onNavigatePrevClicked( event ) {
|
||||
|
||||
event.preventDefault();
|
||||
this.Reveal.onUserInput();
|
||||
|
||||
this.Reveal.prev();
|
||||
|
||||
}
|
||||
|
||||
onNavigateNextClicked( event ) {
|
||||
|
||||
event.preventDefault();
|
||||
this.Reveal.onUserInput();
|
||||
|
||||
this.Reveal.next();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
103
js/controllers/focus.js
Normal file
103
js/controllers/focus.js
Normal file
@ -0,0 +1,103 @@
|
||||
import { closest } from '../utils/util.js'
|
||||
|
||||
/**
|
||||
* Manages focus when a presentation is embedded. This
|
||||
* helps us only capture keyboard from the presentation
|
||||
* a user is currently interacting with in a page where
|
||||
* multiple presentations are embedded.
|
||||
*/
|
||||
|
||||
const STATE_FOCUS = 'focus';
|
||||
const STATE_BLUR = 'blur';
|
||||
|
||||
export default class Focus {
|
||||
|
||||
constructor( Reveal ) {
|
||||
|
||||
this.Reveal = Reveal;
|
||||
|
||||
this.onRevealPointerDown = this.onRevealPointerDown.bind( this );
|
||||
this.onDocumentPointerDown = this.onDocumentPointerDown.bind( this );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the reveal.js config is updated.
|
||||
*/
|
||||
configure( config, oldConfig ) {
|
||||
|
||||
if( config.embedded ) {
|
||||
this.blur();
|
||||
}
|
||||
else {
|
||||
this.focus();
|
||||
this.unbind();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bind() {
|
||||
|
||||
if( this.Reveal.getConfig().embedded ) {
|
||||
this.Reveal.getRevealElement().addEventListener( 'pointerdown', this.onRevealPointerDown, false );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
unbind() {
|
||||
|
||||
this.Reveal.getRevealElement().removeEventListener( 'pointerdown', this.onRevealPointerDown, false );
|
||||
document.removeEventListener( 'pointerdown', this.onDocumentPointerDown, false );
|
||||
|
||||
}
|
||||
|
||||
focus() {
|
||||
|
||||
if( this.state !== STATE_FOCUS ) {
|
||||
this.Reveal.getRevealElement().classList.add( 'focused' );
|
||||
document.addEventListener( 'pointerdown', this.onDocumentPointerDown, false );
|
||||
}
|
||||
|
||||
this.state = STATE_FOCUS;
|
||||
|
||||
}
|
||||
|
||||
blur() {
|
||||
|
||||
if( this.state !== STATE_BLUR ) {
|
||||
this.Reveal.getRevealElement().classList.remove( 'focused' );
|
||||
document.removeEventListener( 'pointerdown', this.onDocumentPointerDown, false );
|
||||
}
|
||||
|
||||
this.state = STATE_BLUR;
|
||||
|
||||
}
|
||||
|
||||
isFocused() {
|
||||
|
||||
return this.state === STATE_FOCUS;
|
||||
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
this.Reveal.getRevealElement().classList.remove( 'focused' );
|
||||
|
||||
}
|
||||
|
||||
onRevealPointerDown( event ) {
|
||||
|
||||
this.focus();
|
||||
|
||||
}
|
||||
|
||||
onDocumentPointerDown( event ) {
|
||||
|
||||
let revealElement = closest( event.target, '.reveal' );
|
||||
if( !revealElement || revealElement !== this.Reveal.getRevealElement() ) {
|
||||
this.blur();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
376
js/controllers/fragments.js
Normal file
376
js/controllers/fragments.js
Normal file
@ -0,0 +1,376 @@
|
||||
import { extend, queryAll } from '../utils/util.js'
|
||||
|
||||
/**
|
||||
* Handles sorting and navigation of slide fragments.
|
||||
* Fragments are elements within a slide that are
|
||||
* revealed/animated incrementally.
|
||||
*/
|
||||
export default class Fragments {
|
||||
|
||||
constructor( Reveal ) {
|
||||
|
||||
this.Reveal = Reveal;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the reveal.js config is updated.
|
||||
*/
|
||||
configure( config, oldConfig ) {
|
||||
|
||||
if( config.fragments === false ) {
|
||||
this.disable();
|
||||
}
|
||||
else if( oldConfig.fragments === false ) {
|
||||
this.enable();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* If fragments are disabled in the deck, they should all be
|
||||
* visible rather than stepped through.
|
||||
*/
|
||||
disable() {
|
||||
|
||||
queryAll( this.Reveal.getSlidesElement(), '.fragment' ).forEach( element => {
|
||||
element.classList.add( 'visible' );
|
||||
element.classList.remove( 'current-fragment' );
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse of #disable(). Only called if fragments have
|
||||
* previously been disabled.
|
||||
*/
|
||||
enable() {
|
||||
|
||||
queryAll( this.Reveal.getSlidesElement(), '.fragment' ).forEach( element => {
|
||||
element.classList.remove( 'visible' );
|
||||
element.classList.remove( 'current-fragment' );
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object describing the available fragment
|
||||
* directions.
|
||||
*
|
||||
* @return {{prev: boolean, next: boolean}}
|
||||
*/
|
||||
availableRoutes() {
|
||||
|
||||
let currentSlide = this.Reveal.getCurrentSlide();
|
||||
if( currentSlide && this.Reveal.getConfig().fragments ) {
|
||||
let fragments = currentSlide.querySelectorAll( '.fragment:not(.disabled)' );
|
||||
let hiddenFragments = currentSlide.querySelectorAll( '.fragment:not(.disabled):not(.visible)' );
|
||||
|
||||
return {
|
||||
prev: fragments.length - hiddenFragments.length > 0,
|
||||
next: !!hiddenFragments.length
|
||||
};
|
||||
}
|
||||
else {
|
||||
return { prev: false, next: false };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a sorted fragments list, ordered by an increasing
|
||||
* "data-fragment-index" attribute.
|
||||
*
|
||||
* Fragments will be revealed in the order that they are returned by
|
||||
* this function, so you can use the index attributes to control the
|
||||
* order of fragment appearance.
|
||||
*
|
||||
* To maintain a sensible default fragment order, fragments are presumed
|
||||
* to be passed in document order. This function adds a "fragment-index"
|
||||
* attribute to each node if such an attribute is not already present,
|
||||
* and sets that attribute to an integer value which is the position of
|
||||
* the fragment within the fragments list.
|
||||
*
|
||||
* @param {object[]|*} fragments
|
||||
* @param {boolean} grouped If true the returned array will contain
|
||||
* nested arrays for all fragments with the same index
|
||||
* @return {object[]} sorted Sorted array of fragments
|
||||
*/
|
||||
sort( fragments, grouped = false ) {
|
||||
|
||||
fragments = Array.from( fragments );
|
||||
|
||||
let ordered = [],
|
||||
unordered = [],
|
||||
sorted = [];
|
||||
|
||||
// Group ordered and unordered elements
|
||||
fragments.forEach( fragment => {
|
||||
if( fragment.hasAttribute( 'data-fragment-index' ) ) {
|
||||
let index = parseInt( fragment.getAttribute( 'data-fragment-index' ), 10 );
|
||||
|
||||
if( !ordered[index] ) {
|
||||
ordered[index] = [];
|
||||
}
|
||||
|
||||
ordered[index].push( fragment );
|
||||
}
|
||||
else {
|
||||
unordered.push( [ fragment ] );
|
||||
}
|
||||
} );
|
||||
|
||||
// Append fragments without explicit indices in their
|
||||
// DOM order
|
||||
ordered = ordered.concat( unordered );
|
||||
|
||||
// Manually count the index up per group to ensure there
|
||||
// are no gaps
|
||||
let index = 0;
|
||||
|
||||
// Push all fragments in their sorted order to an array,
|
||||
// this flattens the groups
|
||||
ordered.forEach( group => {
|
||||
group.forEach( fragment => {
|
||||
sorted.push( fragment );
|
||||
fragment.setAttribute( 'data-fragment-index', index );
|
||||
} );
|
||||
|
||||
index ++;
|
||||
} );
|
||||
|
||||
return grouped === true ? ordered : sorted;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts and formats all of fragments in the
|
||||
* presentation.
|
||||
*/
|
||||
sortAll() {
|
||||
|
||||
this.Reveal.getHorizontalSlides().forEach( horizontalSlide => {
|
||||
|
||||
let verticalSlides = queryAll( horizontalSlide, 'section' );
|
||||
verticalSlides.forEach( ( verticalSlide, y ) => {
|
||||
|
||||
this.sort( verticalSlide.querySelectorAll( '.fragment' ) );
|
||||
|
||||
}, this );
|
||||
|
||||
if( verticalSlides.length === 0 ) this.sort( horizontalSlide.querySelectorAll( '.fragment' ) );
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the fragments on the current slide so that they
|
||||
* have the appropriate classes (.visible + .current-fragment).
|
||||
*
|
||||
* @param {number} [index] The index of the current fragment
|
||||
* @param {array} [fragments] Array containing all fragments
|
||||
* in the current slide
|
||||
*
|
||||
* @return {{shown: array, hidden: array}}
|
||||
*/
|
||||
update( index, fragments ) {
|
||||
|
||||
let changedFragments = {
|
||||
shown: [],
|
||||
hidden: []
|
||||
};
|
||||
|
||||
let currentSlide = this.Reveal.getCurrentSlide();
|
||||
if( currentSlide && this.Reveal.getConfig().fragments ) {
|
||||
|
||||
fragments = fragments || this.sort( currentSlide.querySelectorAll( '.fragment' ) );
|
||||
|
||||
if( fragments.length ) {
|
||||
|
||||
let maxIndex = 0;
|
||||
|
||||
if( typeof index !== 'number' ) {
|
||||
let currentFragment = this.sort( currentSlide.querySelectorAll( '.fragment.visible' ) ).pop();
|
||||
if( currentFragment ) {
|
||||
index = parseInt( currentFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
|
||||
}
|
||||
}
|
||||
|
||||
Array.from( fragments ).forEach( ( el, i ) => {
|
||||
|
||||
if( el.hasAttribute( 'data-fragment-index' ) ) {
|
||||
i = parseInt( el.getAttribute( 'data-fragment-index' ), 10 );
|
||||
}
|
||||
|
||||
maxIndex = Math.max( maxIndex, i );
|
||||
|
||||
// Visible fragments
|
||||
if( i <= index ) {
|
||||
let wasVisible = el.classList.contains( 'visible' )
|
||||
el.classList.add( 'visible' );
|
||||
el.classList.remove( 'current-fragment' );
|
||||
|
||||
if( i === index ) {
|
||||
// Announce the fragments one by one to the Screen Reader
|
||||
this.Reveal.announceStatus( this.Reveal.getStatusText( el ) );
|
||||
|
||||
el.classList.add( 'current-fragment' );
|
||||
this.Reveal.slideContent.startEmbeddedContent( el );
|
||||
}
|
||||
|
||||
if( !wasVisible ) {
|
||||
changedFragments.shown.push( el )
|
||||
this.Reveal.dispatchEvent({
|
||||
target: el,
|
||||
type: 'visible',
|
||||
bubbles: false
|
||||
});
|
||||
}
|
||||
}
|
||||
// Hidden fragments
|
||||
else {
|
||||
let wasVisible = el.classList.contains( 'visible' )
|
||||
el.classList.remove( 'visible' );
|
||||
el.classList.remove( 'current-fragment' );
|
||||
|
||||
if( wasVisible ) {
|
||||
this.Reveal.slideContent.stopEmbeddedContent( el );
|
||||
changedFragments.hidden.push( el );
|
||||
this.Reveal.dispatchEvent({
|
||||
target: el,
|
||||
type: 'hidden',
|
||||
bubbles: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
// Write the current fragment index to the slide <section>.
|
||||
// This can be used by end users to apply styles based on
|
||||
// the current fragment index.
|
||||
index = typeof index === 'number' ? index : -1;
|
||||
index = Math.max( Math.min( index, maxIndex ), -1 );
|
||||
currentSlide.setAttribute( 'data-fragment', index );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return changedFragments;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the fragments on the given slide so that they have
|
||||
* valid indices. Call this if fragments are changed in the DOM
|
||||
* after reveal.js has already initialized.
|
||||
*
|
||||
* @param {HTMLElement} slide
|
||||
* @return {Array} a list of the HTML fragments that were synced
|
||||
*/
|
||||
sync( slide = this.Reveal.getCurrentSlide() ) {
|
||||
|
||||
return this.sort( slide.querySelectorAll( '.fragment' ) );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the specified slide fragment.
|
||||
*
|
||||
* @param {?number} index The index of the fragment that
|
||||
* should be shown, -1 means all are invisible
|
||||
* @param {number} offset Integer offset to apply to the
|
||||
* fragment index
|
||||
*
|
||||
* @return {boolean} true if a change was made in any
|
||||
* fragments visibility as part of this call
|
||||
*/
|
||||
goto( index, offset = 0 ) {
|
||||
|
||||
let currentSlide = this.Reveal.getCurrentSlide();
|
||||
if( currentSlide && this.Reveal.getConfig().fragments ) {
|
||||
|
||||
let fragments = this.sort( currentSlide.querySelectorAll( '.fragment:not(.disabled)' ) );
|
||||
if( fragments.length ) {
|
||||
|
||||
// If no index is specified, find the current
|
||||
if( typeof index !== 'number' ) {
|
||||
let lastVisibleFragment = this.sort( currentSlide.querySelectorAll( '.fragment:not(.disabled).visible' ) ).pop();
|
||||
|
||||
if( lastVisibleFragment ) {
|
||||
index = parseInt( lastVisibleFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
|
||||
}
|
||||
else {
|
||||
index = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the offset if there is one
|
||||
index += offset;
|
||||
|
||||
let changedFragments = this.update( index, fragments );
|
||||
|
||||
if( changedFragments.hidden.length ) {
|
||||
this.Reveal.dispatchEvent({
|
||||
type: 'fragmenthidden',
|
||||
data: {
|
||||
fragment: changedFragments.hidden[0],
|
||||
fragments: changedFragments.hidden
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if( changedFragments.shown.length ) {
|
||||
this.Reveal.dispatchEvent({
|
||||
type: 'fragmentshown',
|
||||
data: {
|
||||
fragment: changedFragments.shown[0],
|
||||
fragments: changedFragments.shown
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.Reveal.controls.update();
|
||||
this.Reveal.progress.update();
|
||||
|
||||
if( this.Reveal.getConfig().fragmentInURL ) {
|
||||
this.Reveal.location.writeURL();
|
||||
}
|
||||
|
||||
return !!( changedFragments.shown.length || changedFragments.hidden.length );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the next slide fragment.
|
||||
*
|
||||
* @return {boolean} true if there was a next fragment,
|
||||
* false otherwise
|
||||
*/
|
||||
next() {
|
||||
|
||||
return this.goto( null, 1 );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the previous slide fragment.
|
||||
*
|
||||
* @return {boolean} true if there was a previous fragment,
|
||||
* false otherwise
|
||||
*/
|
||||
prev() {
|
||||
|
||||
return this.goto( null, -1 );
|
||||
|
||||
}
|
||||
|
||||
}
|
170
js/controllers/jumptoslide.js
Normal file
170
js/controllers/jumptoslide.js
Normal file
@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Makes it possible to jump to a slide by entering its
|
||||
* slide number or id.
|
||||
*/
|
||||
export default class JumpToSlide {
|
||||
|
||||
constructor( Reveal ) {
|
||||
|
||||
this.Reveal = Reveal;
|
||||
|
||||
this.onInput = this.onInput.bind( this );
|
||||
this.onBlur = this.onBlur.bind( this );
|
||||
this.onKeyDown = this.onKeyDown.bind( this );
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
this.element = document.createElement( 'div' );
|
||||
this.element.className = 'jump-to-slide';
|
||||
|
||||
this.jumpInput = document.createElement( 'input' );
|
||||
this.jumpInput.type = 'text';
|
||||
this.jumpInput.className = 'jump-to-slide-input';
|
||||
this.jumpInput.placeholder = 'Jump to slide';
|
||||
this.jumpInput.addEventListener( 'input', this.onInput );
|
||||
this.jumpInput.addEventListener( 'keydown', this.onKeyDown );
|
||||
this.jumpInput.addEventListener( 'blur', this.onBlur );
|
||||
|
||||
this.element.appendChild( this.jumpInput );
|
||||
|
||||
}
|
||||
|
||||
show() {
|
||||
|
||||
this.indicesOnShow = this.Reveal.getIndices();
|
||||
|
||||
this.Reveal.getRevealElement().appendChild( this.element );
|
||||
this.jumpInput.focus();
|
||||
|
||||
}
|
||||
|
||||
hide() {
|
||||
|
||||
if( this.isVisible() ) {
|
||||
this.element.remove();
|
||||
this.jumpInput.value = '';
|
||||
|
||||
clearTimeout( this.jumpTimeout );
|
||||
delete this.jumpTimeout;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
isVisible() {
|
||||
|
||||
return !!this.element.parentNode;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the current input and jumps to the given slide.
|
||||
*/
|
||||
jump() {
|
||||
|
||||
clearTimeout( this.jumpTimeout );
|
||||
delete this.jumpTimeout;
|
||||
|
||||
const query = this.jumpInput.value.trim( '' );
|
||||
let indices = this.Reveal.location.getIndicesFromHash( query, { oneBasedIndex: true } );
|
||||
|
||||
// If no valid index was found and the input query is a
|
||||
// string, fall back on a simple search
|
||||
if( !indices && /\S+/i.test( query ) && query.length > 1 ) {
|
||||
indices = this.search( query );
|
||||
}
|
||||
|
||||
if( indices && query !== '' ) {
|
||||
this.Reveal.slide( indices.h, indices.v, indices.f );
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
this.Reveal.slide( this.indicesOnShow.h, this.indicesOnShow.v, this.indicesOnShow.f );
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
jumpAfter( delay ) {
|
||||
|
||||
clearTimeout( this.jumpTimeout );
|
||||
this.jumpTimeout = setTimeout( () => this.jump(), delay );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A lofi search that looks for the given query in all
|
||||
* of our slides and returns the first match.
|
||||
*/
|
||||
search( query ) {
|
||||
|
||||
const regex = new RegExp( '\\b' + query.trim() + '\\b', 'i' );
|
||||
|
||||
const slide = this.Reveal.getSlides().find( ( slide ) => {
|
||||
return regex.test( slide.innerText );
|
||||
} );
|
||||
|
||||
if( slide ) {
|
||||
return this.Reveal.getIndices( slide );
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts back to the slide we were on when jump to slide was
|
||||
* invoked.
|
||||
*/
|
||||
cancel() {
|
||||
|
||||
this.Reveal.slide( this.indicesOnShow.h, this.indicesOnShow.v, this.indicesOnShow.f );
|
||||
this.hide();
|
||||
|
||||
}
|
||||
|
||||
confirm() {
|
||||
|
||||
this.jump();
|
||||
this.hide();
|
||||
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
this.jumpInput.removeEventListener( 'input', this.onInput );
|
||||
this.jumpInput.removeEventListener( 'keydown', this.onKeyDown );
|
||||
this.jumpInput.removeEventListener( 'blur', this.onBlur );
|
||||
|
||||
this.element.remove();
|
||||
|
||||
}
|
||||
|
||||
onKeyDown( event ) {
|
||||
|
||||
if( event.keyCode === 13 ) {
|
||||
this.confirm();
|
||||
}
|
||||
else if( event.keyCode === 27 ) {
|
||||
this.cancel();
|
||||
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onInput( event ) {
|
||||
|
||||
this.jumpAfter( 200 );
|
||||
|
||||
}
|
||||
|
||||
onBlur() {
|
||||
|
||||
setTimeout( () => this.hide(), 1 );
|
||||
|
||||
}
|
||||
|
||||
}
|
399
js/controllers/keyboard.js
Normal file
399
js/controllers/keyboard.js
Normal file
@ -0,0 +1,399 @@
|
||||
import { enterFullscreen } from '../utils/util.js'
|
||||
|
||||
/**
|
||||
* Handles all reveal.js keyboard interactions.
|
||||
*/
|
||||
export default class Keyboard {
|
||||
|
||||
constructor( Reveal ) {
|
||||
|
||||
this.Reveal = Reveal;
|
||||
|
||||
// A key:value map of keyboard keys and descriptions of
|
||||
// the actions they trigger
|
||||
this.shortcuts = {};
|
||||
|
||||
// Holds custom key code mappings
|
||||
this.bindings = {};
|
||||
|
||||
this.onDocumentKeyDown = this.onDocumentKeyDown.bind( this );
|
||||
this.onDocumentKeyPress = this.onDocumentKeyPress.bind( this );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the reveal.js config is updated.
|
||||
*/
|
||||
configure( config, oldConfig ) {
|
||||
|
||||
if( config.navigationMode === 'linear' ) {
|
||||
this.shortcuts['→ , ↓ , SPACE , N , L , J'] = 'Next slide';
|
||||
this.shortcuts['← , ↑ , P , H , K'] = 'Previous slide';
|
||||
}
|
||||
else {
|
||||
this.shortcuts['N , SPACE'] = 'Next slide';
|
||||
this.shortcuts['P , Shift SPACE'] = 'Previous slide';
|
||||
this.shortcuts['← , H'] = 'Navigate left';
|
||||
this.shortcuts['→ , L'] = 'Navigate right';
|
||||
this.shortcuts['↑ , K'] = 'Navigate up';
|
||||
this.shortcuts['↓ , J'] = 'Navigate down';
|
||||
}
|
||||
|
||||
this.shortcuts['Alt + ←/↑/→/↓'] = 'Navigate without fragments';
|
||||
this.shortcuts['Shift + ←/↑/→/↓'] = 'Jump to first/last slide';
|
||||
this.shortcuts['B , .'] = 'Pause';
|
||||
this.shortcuts['F'] = 'Fullscreen';
|
||||
this.shortcuts['G'] = 'Jump to slide';
|
||||
this.shortcuts['ESC, O'] = 'Slide overview';
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts listening for keyboard events.
|
||||
*/
|
||||
bind() {
|
||||
|
||||
document.addEventListener( 'keydown', this.onDocumentKeyDown, false );
|
||||
document.addEventListener( 'keypress', this.onDocumentKeyPress, false );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops listening for keyboard events.
|
||||
*/
|
||||
unbind() {
|
||||
|
||||
document.removeEventListener( 'keydown', this.onDocumentKeyDown, false );
|
||||
document.removeEventListener( 'keypress', this.onDocumentKeyPress, false );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom key binding with optional description to
|
||||
* be added to the help screen.
|
||||
*/
|
||||
addKeyBinding( binding, callback ) {
|
||||
|
||||
if( typeof binding === 'object' && binding.keyCode ) {
|
||||
this.bindings[binding.keyCode] = {
|
||||
callback: callback,
|
||||
key: binding.key,
|
||||
description: binding.description
|
||||
};
|
||||
}
|
||||
else {
|
||||
this.bindings[binding] = {
|
||||
callback: callback,
|
||||
key: null,
|
||||
description: null
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the specified custom key binding.
|
||||
*/
|
||||
removeKeyBinding( keyCode ) {
|
||||
|
||||
delete this.bindings[keyCode];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Programmatically triggers a keyboard event
|
||||
*
|
||||
* @param {int} keyCode
|
||||
*/
|
||||
triggerKey( keyCode ) {
|
||||
|
||||
this.onDocumentKeyDown( { keyCode } );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new shortcut to include in the help overlay
|
||||
*
|
||||
* @param {String} key
|
||||
* @param {String} value
|
||||
*/
|
||||
registerKeyboardShortcut( key, value ) {
|
||||
|
||||
this.shortcuts[key] = value;
|
||||
|
||||
}
|
||||
|
||||
getShortcuts() {
|
||||
|
||||
return this.shortcuts;
|
||||
|
||||
}
|
||||
|
||||
getBindings() {
|
||||
|
||||
return this.bindings;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the document level 'keypress' event.
|
||||
*
|
||||
* @param {object} event
|
||||
*/
|
||||
onDocumentKeyPress( event ) {
|
||||
|
||||
// Check if the pressed key is question mark
|
||||
if( event.shiftKey && event.charCode === 63 ) {
|
||||
this.Reveal.toggleHelp();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the document level 'keydown' event.
|
||||
*
|
||||
* @param {object} event
|
||||
*/
|
||||
onDocumentKeyDown( event ) {
|
||||
|
||||
let config = this.Reveal.getConfig();
|
||||
|
||||
// If there's a condition specified and it returns false,
|
||||
// ignore this event
|
||||
if( typeof config.keyboardCondition === 'function' && config.keyboardCondition(event) === false ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If keyboardCondition is set, only capture keyboard events
|
||||
// for embedded decks when they are focused
|
||||
if( config.keyboardCondition === 'focused' && !this.Reveal.isFocused() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Shorthand
|
||||
let keyCode = event.keyCode;
|
||||
|
||||
// Remember if auto-sliding was paused so we can toggle it
|
||||
let autoSlideWasPaused = !this.Reveal.isAutoSliding();
|
||||
|
||||
this.Reveal.onUserInput( event );
|
||||
|
||||
// Is there a focused element that could be using the keyboard?
|
||||
let activeElementIsCE = document.activeElement && document.activeElement.isContentEditable === true;
|
||||
let activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );
|
||||
let activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className);
|
||||
|
||||
// Whitelist certain modifiers for slide navigation shortcuts
|
||||
let isNavigationKey = [32, 37, 38, 39, 40, 78, 80].indexOf( event.keyCode ) !== -1;
|
||||
|
||||
// Prevent all other events when a modifier is pressed
|
||||
let unusedModifier = !( isNavigationKey && event.shiftKey || event.altKey ) &&
|
||||
( event.shiftKey || event.altKey || event.ctrlKey || event.metaKey );
|
||||
|
||||
// Disregard the event if there's a focused element or a
|
||||
// keyboard modifier key is present
|
||||
if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || unusedModifier ) return;
|
||||
|
||||
// While paused only allow resume keyboard events; 'b', 'v', '.'
|
||||
let resumeKeyCodes = [66,86,190,191];
|
||||
let key;
|
||||
|
||||
// Custom key bindings for togglePause should be able to resume
|
||||
if( typeof config.keyboard === 'object' ) {
|
||||
for( key in config.keyboard ) {
|
||||
if( config.keyboard[key] === 'togglePause' ) {
|
||||
resumeKeyCodes.push( parseInt( key, 10 ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( this.Reveal.isPaused() && resumeKeyCodes.indexOf( keyCode ) === -1 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use linear navigation if we're configured to OR if
|
||||
// the presentation is one-dimensional
|
||||
let useLinearMode = config.navigationMode === 'linear' || !this.Reveal.hasHorizontalSlides() || !this.Reveal.hasVerticalSlides();
|
||||
|
||||
let triggered = false;
|
||||
|
||||
// 1. User defined key bindings
|
||||
if( typeof config.keyboard === 'object' ) {
|
||||
|
||||
for( key in config.keyboard ) {
|
||||
|
||||
// Check if this binding matches the pressed key
|
||||
if( parseInt( key, 10 ) === keyCode ) {
|
||||
|
||||
let value = config.keyboard[ key ];
|
||||
|
||||
// Callback function
|
||||
if( typeof value === 'function' ) {
|
||||
value.apply( null, [ event ] );
|
||||
}
|
||||
// String shortcuts to reveal.js API
|
||||
else if( typeof value === 'string' && typeof this.Reveal[ value ] === 'function' ) {
|
||||
this.Reveal[ value ].call();
|
||||
}
|
||||
|
||||
triggered = true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 2. Registered custom key bindings
|
||||
if( triggered === false ) {
|
||||
|
||||
for( key in this.bindings ) {
|
||||
|
||||
// Check if this binding matches the pressed key
|
||||
if( parseInt( key, 10 ) === keyCode ) {
|
||||
|
||||
let action = this.bindings[ key ].callback;
|
||||
|
||||
// Callback function
|
||||
if( typeof action === 'function' ) {
|
||||
action.apply( null, [ event ] );
|
||||
}
|
||||
// String shortcuts to reveal.js API
|
||||
else if( typeof action === 'string' && typeof this.Reveal[ action ] === 'function' ) {
|
||||
this.Reveal[ action ].call();
|
||||
}
|
||||
|
||||
triggered = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. System defined key bindings
|
||||
if( triggered === false ) {
|
||||
|
||||
// Assume true and try to prove false
|
||||
triggered = true;
|
||||
|
||||
// P, PAGE UP
|
||||
if( keyCode === 80 || keyCode === 33 ) {
|
||||
this.Reveal.prev({skipFragments: event.altKey});
|
||||
}
|
||||
// N, PAGE DOWN
|
||||
else if( keyCode === 78 || keyCode === 34 ) {
|
||||
this.Reveal.next({skipFragments: event.altKey});
|
||||
}
|
||||
// H, LEFT
|
||||
else if( keyCode === 72 || keyCode === 37 ) {
|
||||
if( event.shiftKey ) {
|
||||
this.Reveal.slide( 0 );
|
||||
}
|
||||
else if( !this.Reveal.overview.isActive() && useLinearMode ) {
|
||||
this.Reveal.prev({skipFragments: event.altKey});
|
||||
}
|
||||
else {
|
||||
this.Reveal.left({skipFragments: event.altKey});
|
||||
}
|
||||
}
|
||||
// L, RIGHT
|
||||
else if( keyCode === 76 || keyCode === 39 ) {
|
||||
if( event.shiftKey ) {
|
||||
this.Reveal.slide( this.Reveal.getHorizontalSlides().length - 1 );
|
||||
}
|
||||
else if( !this.Reveal.overview.isActive() && useLinearMode ) {
|
||||
this.Reveal.next({skipFragments: event.altKey});
|
||||
}
|
||||
else {
|
||||
this.Reveal.right({skipFragments: event.altKey});
|
||||
}
|
||||
}
|
||||
// K, UP
|
||||
else if( keyCode === 75 || keyCode === 38 ) {
|
||||
if( event.shiftKey ) {
|
||||
this.Reveal.slide( undefined, 0 );
|
||||
}
|
||||
else if( !this.Reveal.overview.isActive() && useLinearMode ) {
|
||||
this.Reveal.prev({skipFragments: event.altKey});
|
||||
}
|
||||
else {
|
||||
this.Reveal.up({skipFragments: event.altKey});
|
||||
}
|
||||
}
|
||||
// J, DOWN
|
||||
else if( keyCode === 74 || keyCode === 40 ) {
|
||||
if( event.shiftKey ) {
|
||||
this.Reveal.slide( undefined, Number.MAX_VALUE );
|
||||
}
|
||||
else if( !this.Reveal.overview.isActive() && useLinearMode ) {
|
||||
this.Reveal.next({skipFragments: event.altKey});
|
||||
}
|
||||
else {
|
||||
this.Reveal.down({skipFragments: event.altKey});
|
||||
}
|
||||
}
|
||||
// HOME
|
||||
else if( keyCode === 36 ) {
|
||||
this.Reveal.slide( 0 );
|
||||
}
|
||||
// END
|
||||
else if( keyCode === 35 ) {
|
||||
this.Reveal.slide( this.Reveal.getHorizontalSlides().length - 1 );
|
||||
}
|
||||
// SPACE
|
||||
else if( keyCode === 32 ) {
|
||||
if( this.Reveal.overview.isActive() ) {
|
||||
this.Reveal.overview.deactivate();
|
||||
}
|
||||
if( event.shiftKey ) {
|
||||
this.Reveal.prev({skipFragments: event.altKey});
|
||||
}
|
||||
else {
|
||||
this.Reveal.next({skipFragments: event.altKey});
|
||||
}
|
||||
}
|
||||
// TWO-SPOT, SEMICOLON, B, V, PERIOD, LOGITECH PRESENTER TOOLS "BLACK SCREEN" BUTTON
|
||||
else if( keyCode === 58 || keyCode === 59 || keyCode === 66 || keyCode === 86 || keyCode === 190 || keyCode === 191 ) {
|
||||
this.Reveal.togglePause();
|
||||
}
|
||||
// F
|
||||
else if( keyCode === 70 ) {
|
||||
enterFullscreen( config.embedded ? this.Reveal.getViewportElement() : document.documentElement );
|
||||
}
|
||||
// A
|
||||
else if( keyCode === 65 ) {
|
||||
if ( config.autoSlideStoppable ) {
|
||||
this.Reveal.toggleAutoSlide( autoSlideWasPaused );
|
||||
}
|
||||
}
|
||||
// G
|
||||
else if( keyCode === 71 ) {
|
||||
if ( config.jumpToSlide ) {
|
||||
this.Reveal.toggleJumpToSlide();
|
||||
}
|
||||
}
|
||||
else {
|
||||
triggered = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// If the input resulted in a triggered action we should prevent
|
||||
// the browsers default behavior
|
||||
if( triggered ) {
|
||||
event.preventDefault && event.preventDefault();
|
||||
}
|
||||
// ESC or O key
|
||||
else if( keyCode === 27 || keyCode === 79 ) {
|
||||
if( this.Reveal.closeOverlay() === false ) {
|
||||
this.Reveal.overview.toggle();
|
||||
}
|
||||
|
||||
event.preventDefault && event.preventDefault();
|
||||
}
|
||||
|
||||
// If auto-sliding is enabled we need to cue up
|
||||
// another timeout
|
||||
this.Reveal.cueAutoSlide();
|
||||
|
||||
}
|
||||
|
||||
}
|
245
js/controllers/location.js
Normal file
245
js/controllers/location.js
Normal file
@ -0,0 +1,245 @@
|
||||
/**
|
||||
* Reads and writes the URL based on reveal.js' current state.
|
||||
*/
|
||||
export default class Location {
|
||||
|
||||
// The minimum number of milliseconds that must pass between
|
||||
// calls to history.replaceState
|
||||
MAX_REPLACE_STATE_FREQUENCY = 1000
|
||||
|
||||
constructor( Reveal ) {
|
||||
|
||||
this.Reveal = Reveal;
|
||||
|
||||
// Delays updates to the URL due to a Chrome thumbnailer bug
|
||||
this.writeURLTimeout = 0;
|
||||
|
||||
this.replaceStateTimestamp = 0;
|
||||
|
||||
this.onWindowHashChange = this.onWindowHashChange.bind( this );
|
||||
|
||||
}
|
||||
|
||||
bind() {
|
||||
|
||||
window.addEventListener( 'hashchange', this.onWindowHashChange, false );
|
||||
|
||||
}
|
||||
|
||||
unbind() {
|
||||
|
||||
window.removeEventListener( 'hashchange', this.onWindowHashChange, false );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the slide indices for the given hash link.
|
||||
*
|
||||
* @param {string} [hash] the hash string that we want to
|
||||
* find the indices for
|
||||
*
|
||||
* @returns slide indices or null
|
||||
*/
|
||||
getIndicesFromHash( hash=window.location.hash, options={} ) {
|
||||
|
||||
// Attempt to parse the hash as either an index or name
|
||||
let name = hash.replace( /^#\/?/, '' );
|
||||
let bits = name.split( '/' );
|
||||
|
||||
// If the first bit is not fully numeric and there is a name we
|
||||
// can assume that this is a named link
|
||||
if( !/^[0-9]*$/.test( bits[0] ) && name.length ) {
|
||||
let element;
|
||||
|
||||
let f;
|
||||
|
||||
// Parse named links with fragments (#/named-link/2)
|
||||
if( /\/[-\d]+$/g.test( name ) ) {
|
||||
f = parseInt( name.split( '/' ).pop(), 10 );
|
||||
f = isNaN(f) ? undefined : f;
|
||||
name = name.split( '/' ).shift();
|
||||
}
|
||||
|
||||
// Ensure the named link is a valid HTML ID attribute
|
||||
try {
|
||||
element = document.getElementById( decodeURIComponent( name ) );
|
||||
}
|
||||
catch ( error ) { }
|
||||
|
||||
if( element ) {
|
||||
return { ...this.Reveal.getIndices( element ), f };
|
||||
}
|
||||
}
|
||||
else {
|
||||
const config = this.Reveal.getConfig();
|
||||
let hashIndexBase = config.hashOneBasedIndex || options.oneBasedIndex ? 1 : 0;
|
||||
|
||||
// Read the index components of the hash
|
||||
let h = ( parseInt( bits[0], 10 ) - hashIndexBase ) || 0,
|
||||
v = ( parseInt( bits[1], 10 ) - hashIndexBase ) || 0,
|
||||
f;
|
||||
|
||||
if( config.fragmentInURL ) {
|
||||
f = parseInt( bits[2], 10 );
|
||||
if( isNaN( f ) ) {
|
||||
f = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return { h, v, f };
|
||||
}
|
||||
|
||||
// The hash couldn't be parsed or no matching named link was found
|
||||
return null
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the current URL (hash) and navigates accordingly.
|
||||
*/
|
||||
readURL() {
|
||||
|
||||
const currentIndices = this.Reveal.getIndices();
|
||||
const newIndices = this.getIndicesFromHash();
|
||||
|
||||
if( newIndices ) {
|
||||
if( ( newIndices.h !== currentIndices.h || newIndices.v !== currentIndices.v || newIndices.f !== undefined ) ) {
|
||||
this.Reveal.slide( newIndices.h, newIndices.v, newIndices.f );
|
||||
}
|
||||
}
|
||||
// If no new indices are available, we're trying to navigate to
|
||||
// a slide hash that does not exist
|
||||
else {
|
||||
this.Reveal.slide( currentIndices.h || 0, currentIndices.v || 0 );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the page URL (hash) to reflect the current
|
||||
* state.
|
||||
*
|
||||
* @param {number} delay The time in ms to wait before
|
||||
* writing the hash
|
||||
*/
|
||||
writeURL( delay ) {
|
||||
|
||||
let config = this.Reveal.getConfig();
|
||||
let currentSlide = this.Reveal.getCurrentSlide();
|
||||
|
||||
// Make sure there's never more than one timeout running
|
||||
clearTimeout( this.writeURLTimeout );
|
||||
|
||||
// If a delay is specified, timeout this call
|
||||
if( typeof delay === 'number' ) {
|
||||
this.writeURLTimeout = setTimeout( this.writeURL, delay );
|
||||
}
|
||||
else if( currentSlide ) {
|
||||
|
||||
let hash = this.getHash();
|
||||
|
||||
// If we're configured to push to history OR the history
|
||||
// API is not available.
|
||||
if( config.history ) {
|
||||
window.location.hash = hash;
|
||||
}
|
||||
// If we're configured to reflect the current slide in the
|
||||
// URL without pushing to history.
|
||||
else if( config.hash ) {
|
||||
// If the hash is empty, don't add it to the URL
|
||||
if( hash === '/' ) {
|
||||
this.debouncedReplaceState( window.location.pathname + window.location.search );
|
||||
}
|
||||
else {
|
||||
this.debouncedReplaceState( '#' + hash );
|
||||
}
|
||||
}
|
||||
// UPDATE: The below nuking of all hash changes breaks
|
||||
// anchors on pages where reveal.js is running. Removed
|
||||
// in 4.0. Why was it here in the first place? ¯\_(ツ)_/¯
|
||||
//
|
||||
// If history and hash are both disabled, a hash may still
|
||||
// be added to the URL by clicking on a href with a hash
|
||||
// target. Counter this by always removing the hash.
|
||||
// else {
|
||||
// window.history.replaceState( null, null, window.location.pathname + window.location.search );
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
replaceState( url ) {
|
||||
|
||||
window.history.replaceState( null, null, url );
|
||||
this.replaceStateTimestamp = Date.now();
|
||||
|
||||
}
|
||||
|
||||
debouncedReplaceState( url ) {
|
||||
|
||||
clearTimeout( this.replaceStateTimeout );
|
||||
|
||||
if( Date.now() - this.replaceStateTimestamp > this.MAX_REPLACE_STATE_FREQUENCY ) {
|
||||
this.replaceState( url );
|
||||
}
|
||||
else {
|
||||
this.replaceStateTimeout = setTimeout( () => this.replaceState( url ), this.MAX_REPLACE_STATE_FREQUENCY );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a hash URL that will resolve to the given slide location.
|
||||
*
|
||||
* @param {HTMLElement} [slide=currentSlide] The slide to link to
|
||||
*/
|
||||
getHash( slide ) {
|
||||
|
||||
let url = '/';
|
||||
|
||||
// Attempt to create a named link based on the slide's ID
|
||||
let s = slide || this.Reveal.getCurrentSlide();
|
||||
let id = s ? s.getAttribute( 'id' ) : null;
|
||||
if( id ) {
|
||||
id = encodeURIComponent( id );
|
||||
}
|
||||
|
||||
let index = this.Reveal.getIndices( slide );
|
||||
if( !this.Reveal.getConfig().fragmentInURL ) {
|
||||
index.f = undefined;
|
||||
}
|
||||
|
||||
// If the current slide has an ID, use that as a named link,
|
||||
// but we don't support named links with a fragment index
|
||||
if( typeof id === 'string' && id.length ) {
|
||||
url = '/' + id;
|
||||
|
||||
// If there is also a fragment, append that at the end
|
||||
// of the named link, like: #/named-link/2
|
||||
if( index.f >= 0 ) url += '/' + index.f;
|
||||
}
|
||||
// Otherwise use the /h/v index
|
||||
else {
|
||||
let hashIndexBase = this.Reveal.getConfig().hashOneBasedIndex ? 1 : 0;
|
||||
if( index.h > 0 || index.v > 0 || index.f >= 0 ) url += index.h + hashIndexBase;
|
||||
if( index.v > 0 || index.f >= 0 ) url += '/' + (index.v + hashIndexBase );
|
||||
if( index.f >= 0 ) url += '/' + index.f;
|
||||
}
|
||||
|
||||
return url;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the window level 'hashchange' event.
|
||||
*
|
||||
* @param {object} [event]
|
||||
*/
|
||||
onWindowHashChange( event ) {
|
||||
|
||||
this.readURL();
|
||||
|
||||
}
|
||||
|
||||
}
|
120
js/controllers/notes.js
Normal file
120
js/controllers/notes.js
Normal file
@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Handles the showing of speaker notes
|
||||
*/
|
||||
export default class Notes {
|
||||
|
||||
constructor( Reveal ) {
|
||||
|
||||
this.Reveal = Reveal;
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
this.element = document.createElement( 'div' );
|
||||
this.element.className = 'speaker-notes';
|
||||
this.element.setAttribute( 'data-prevent-swipe', '' );
|
||||
this.element.setAttribute( 'tabindex', '0' );
|
||||
this.Reveal.getRevealElement().appendChild( this.element );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the reveal.js config is updated.
|
||||
*/
|
||||
configure( config, oldConfig ) {
|
||||
|
||||
if( config.showNotes ) {
|
||||
this.element.setAttribute( 'data-layout', typeof config.showNotes === 'string' ? config.showNotes : 'inline' );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick up notes from the current slide and display them
|
||||
* to the viewer.
|
||||
*
|
||||
* @see {@link config.showNotes}
|
||||
*/
|
||||
update() {
|
||||
|
||||
if( this.Reveal.getConfig().showNotes && this.element && this.Reveal.getCurrentSlide() && !this.Reveal.print.isPrintingPDF() ) {
|
||||
|
||||
this.element.innerHTML = this.getSlideNotes() || '<span class="notes-placeholder">No notes on this slide.</span>';
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the visibility of the speaker notes sidebar that
|
||||
* is used to share annotated slides. The notes sidebar is
|
||||
* only visible if showNotes is true and there are notes on
|
||||
* one or more slides in the deck.
|
||||
*/
|
||||
updateVisibility() {
|
||||
|
||||
if( this.Reveal.getConfig().showNotes && this.hasNotes() && !this.Reveal.print.isPrintingPDF() ) {
|
||||
this.Reveal.getRevealElement().classList.add( 'show-notes' );
|
||||
}
|
||||
else {
|
||||
this.Reveal.getRevealElement().classList.remove( 'show-notes' );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there are speaker notes for ANY slide in the
|
||||
* presentation.
|
||||
*/
|
||||
hasNotes() {
|
||||
|
||||
return this.Reveal.getSlidesElement().querySelectorAll( '[data-notes], aside.notes' ).length > 0;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this presentation is running inside of the
|
||||
* speaker notes window.
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
isSpeakerNotesWindow() {
|
||||
|
||||
return !!window.location.search.match( /receiver/gi );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the speaker notes from a slide. Notes can be
|
||||
* defined in two ways:
|
||||
* 1. As a data-notes attribute on the slide <section>
|
||||
* 2. With <aside class="notes"> elements inside the slide
|
||||
*
|
||||
* @param {HTMLElement} [slide=currentSlide]
|
||||
* @return {(string|null)}
|
||||
*/
|
||||
getSlideNotes( slide = this.Reveal.getCurrentSlide() ) {
|
||||
|
||||
// Notes can be specified via the data-notes attribute...
|
||||
if( slide.hasAttribute( 'data-notes' ) ) {
|
||||
return slide.getAttribute( 'data-notes' );
|
||||
}
|
||||
|
||||
// ... or using <aside class="notes"> elements
|
||||
let notesElements = slide.querySelectorAll( 'aside.notes' );
|
||||
if( notesElements ) {
|
||||
return Array.from(notesElements).map( notesElement => notesElement.innerHTML ).join( '\n' );
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
this.element.remove();
|
||||
|
||||
}
|
||||
|
||||
}
|
255
js/controllers/overview.js
Normal file
255
js/controllers/overview.js
Normal file
@ -0,0 +1,255 @@
|
||||
import { SLIDES_SELECTOR } from '../utils/constants.js'
|
||||
import { extend, queryAll, transformElement } from '../utils/util.js'
|
||||
|
||||
/**
|
||||
* Handles all logic related to the overview mode
|
||||
* (birds-eye view of all slides).
|
||||
*/
|
||||
export default class Overview {
|
||||
|
||||
constructor( Reveal ) {
|
||||
|
||||
this.Reveal = Reveal;
|
||||
|
||||
this.active = false;
|
||||
|
||||
this.onSlideClicked = this.onSlideClicked.bind( this );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the overview of slides (quick nav) by scaling
|
||||
* down and arranging all slide elements.
|
||||
*/
|
||||
activate() {
|
||||
|
||||
// Only proceed if enabled in config
|
||||
if( this.Reveal.getConfig().overview && !this.isActive() ) {
|
||||
|
||||
this.active = true;
|
||||
|
||||
this.Reveal.getRevealElement().classList.add( 'overview' );
|
||||
|
||||
// Don't auto-slide while in overview mode
|
||||
this.Reveal.cancelAutoSlide();
|
||||
|
||||
// Move the backgrounds element into the slide container to
|
||||
// that the same scaling is applied
|
||||
this.Reveal.getSlidesElement().appendChild( this.Reveal.getBackgroundsElement() );
|
||||
|
||||
// Clicking on an overview slide navigates to it
|
||||
queryAll( this.Reveal.getRevealElement(), SLIDES_SELECTOR ).forEach( slide => {
|
||||
if( !slide.classList.contains( 'stack' ) ) {
|
||||
slide.addEventListener( 'click', this.onSlideClicked, true );
|
||||
}
|
||||
} );
|
||||
|
||||
// Calculate slide sizes
|
||||
const margin = 70;
|
||||
const slideSize = this.Reveal.getComputedSlideSize();
|
||||
this.overviewSlideWidth = slideSize.width + margin;
|
||||
this.overviewSlideHeight = slideSize.height + margin;
|
||||
|
||||
// Reverse in RTL mode
|
||||
if( this.Reveal.getConfig().rtl ) {
|
||||
this.overviewSlideWidth = -this.overviewSlideWidth;
|
||||
}
|
||||
|
||||
this.Reveal.updateSlidesVisibility();
|
||||
|
||||
this.layout();
|
||||
this.update();
|
||||
|
||||
this.Reveal.layout();
|
||||
|
||||
const indices = this.Reveal.getIndices();
|
||||
|
||||
// Notify observers of the overview showing
|
||||
this.Reveal.dispatchEvent({
|
||||
type: 'overviewshown',
|
||||
data: {
|
||||
'indexh': indices.h,
|
||||
'indexv': indices.v,
|
||||
'currentSlide': this.Reveal.getCurrentSlide()
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses CSS transforms to position all slides in a grid for
|
||||
* display inside of the overview mode.
|
||||
*/
|
||||
layout() {
|
||||
|
||||
// Layout slides
|
||||
this.Reveal.getHorizontalSlides().forEach( ( hslide, h ) => {
|
||||
hslide.setAttribute( 'data-index-h', h );
|
||||
transformElement( hslide, 'translate3d(' + ( h * this.overviewSlideWidth ) + 'px, 0, 0)' );
|
||||
|
||||
if( hslide.classList.contains( 'stack' ) ) {
|
||||
|
||||
queryAll( hslide, 'section' ).forEach( ( vslide, v ) => {
|
||||
vslide.setAttribute( 'data-index-h', h );
|
||||
vslide.setAttribute( 'data-index-v', v );
|
||||
|
||||
transformElement( vslide, 'translate3d(0, ' + ( v * this.overviewSlideHeight ) + 'px, 0)' );
|
||||
} );
|
||||
|
||||
}
|
||||
} );
|
||||
|
||||
// Layout slide backgrounds
|
||||
Array.from( this.Reveal.getBackgroundsElement().childNodes ).forEach( ( hbackground, h ) => {
|
||||
transformElement( hbackground, 'translate3d(' + ( h * this.overviewSlideWidth ) + 'px, 0, 0)' );
|
||||
|
||||
queryAll( hbackground, '.slide-background' ).forEach( ( vbackground, v ) => {
|
||||
transformElement( vbackground, 'translate3d(0, ' + ( v * this.overviewSlideHeight ) + 'px, 0)' );
|
||||
} );
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the overview viewport to the current slides.
|
||||
* Called each time the current slide changes.
|
||||
*/
|
||||
update() {
|
||||
|
||||
const vmin = Math.min( window.innerWidth, window.innerHeight );
|
||||
const scale = Math.max( vmin / 5, 150 ) / vmin;
|
||||
const indices = this.Reveal.getIndices();
|
||||
|
||||
this.Reveal.transformSlides( {
|
||||
overview: [
|
||||
'scale('+ scale +')',
|
||||
'translateX('+ ( -indices.h * this.overviewSlideWidth ) +'px)',
|
||||
'translateY('+ ( -indices.v * this.overviewSlideHeight ) +'px)'
|
||||
].join( ' ' )
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Exits the slide overview and enters the currently
|
||||
* active slide.
|
||||
*/
|
||||
deactivate() {
|
||||
|
||||
// Only proceed if enabled in config
|
||||
if( this.Reveal.getConfig().overview ) {
|
||||
|
||||
this.active = false;
|
||||
|
||||
this.Reveal.getRevealElement().classList.remove( 'overview' );
|
||||
|
||||
// Temporarily add a class so that transitions can do different things
|
||||
// depending on whether they are exiting/entering overview, or just
|
||||
// moving from slide to slide
|
||||
this.Reveal.getRevealElement().classList.add( 'overview-deactivating' );
|
||||
|
||||
setTimeout( () => {
|
||||
this.Reveal.getRevealElement().classList.remove( 'overview-deactivating' );
|
||||
}, 1 );
|
||||
|
||||
// Move the background element back out
|
||||
this.Reveal.getRevealElement().appendChild( this.Reveal.getBackgroundsElement() );
|
||||
|
||||
// Clean up changes made to slides
|
||||
queryAll( this.Reveal.getRevealElement(), SLIDES_SELECTOR ).forEach( slide => {
|
||||
transformElement( slide, '' );
|
||||
|
||||
slide.removeEventListener( 'click', this.onSlideClicked, true );
|
||||
} );
|
||||
|
||||
// Clean up changes made to backgrounds
|
||||
queryAll( this.Reveal.getBackgroundsElement(), '.slide-background' ).forEach( background => {
|
||||
transformElement( background, '' );
|
||||
} );
|
||||
|
||||
this.Reveal.transformSlides( { overview: '' } );
|
||||
|
||||
const indices = this.Reveal.getIndices();
|
||||
|
||||
this.Reveal.slide( indices.h, indices.v );
|
||||
this.Reveal.layout();
|
||||
this.Reveal.cueAutoSlide();
|
||||
|
||||
// Notify observers of the overview hiding
|
||||
this.Reveal.dispatchEvent({
|
||||
type: 'overviewhidden',
|
||||
data: {
|
||||
'indexh': indices.h,
|
||||
'indexv': indices.v,
|
||||
'currentSlide': this.Reveal.getCurrentSlide()
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the slide overview mode on and off.
|
||||
*
|
||||
* @param {Boolean} [override] Flag which overrides the
|
||||
* toggle logic and forcibly sets the desired state. True means
|
||||
* overview is open, false means it's closed.
|
||||
*/
|
||||
toggle( override ) {
|
||||
|
||||
if( typeof override === 'boolean' ) {
|
||||
override ? this.activate() : this.deactivate();
|
||||
}
|
||||
else {
|
||||
this.isActive() ? this.deactivate() : this.activate();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the overview is currently active.
|
||||
*
|
||||
* @return {Boolean} true if the overview is active,
|
||||
* false otherwise
|
||||
*/
|
||||
isActive() {
|
||||
|
||||
return this.active;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a slide is and we're in the overview.
|
||||
*
|
||||
* @param {object} event
|
||||
*/
|
||||
onSlideClicked( event ) {
|
||||
|
||||
if( this.isActive() ) {
|
||||
event.preventDefault();
|
||||
|
||||
let element = event.target;
|
||||
|
||||
while( element && !element.nodeName.match( /section/gi ) ) {
|
||||
element = element.parentNode;
|
||||
}
|
||||
|
||||
if( element && !element.classList.contains( 'disabled' ) ) {
|
||||
|
||||
this.deactivate();
|
||||
|
||||
if( element.nodeName.match( /section/gi ) ) {
|
||||
let h = parseInt( element.getAttribute( 'data-index-h' ), 10 ),
|
||||
v = parseInt( element.getAttribute( 'data-index-v' ), 10 );
|
||||
|
||||
this.Reveal.slide( h, v );
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
254
js/controllers/plugins.js
Normal file
254
js/controllers/plugins.js
Normal file
@ -0,0 +1,254 @@
|
||||
import { loadScript } from '../utils/loader.js'
|
||||
|
||||
/**
|
||||
* Manages loading and registering of reveal.js plugins.
|
||||
*/
|
||||
export default class Plugins {
|
||||
|
||||
constructor( reveal ) {
|
||||
|
||||
this.Reveal = reveal;
|
||||
|
||||
// Flags our current state (idle -> loading -> loaded)
|
||||
this.state = 'idle';
|
||||
|
||||
// An id:instance map of currently registered plugins
|
||||
this.registeredPlugins = {};
|
||||
|
||||
this.asyncDependencies = [];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads reveal.js dependencies, registers and
|
||||
* initializes plugins.
|
||||
*
|
||||
* Plugins are direct references to a reveal.js plugin
|
||||
* object that we register and initialize after any
|
||||
* synchronous dependencies have loaded.
|
||||
*
|
||||
* Dependencies are defined via the 'dependencies' config
|
||||
* option and will be loaded prior to starting reveal.js.
|
||||
* Some dependencies may have an 'async' flag, if so they
|
||||
* will load after reveal.js has been started up.
|
||||
*/
|
||||
load( plugins, dependencies ) {
|
||||
|
||||
this.state = 'loading';
|
||||
|
||||
plugins.forEach( this.registerPlugin.bind( this ) );
|
||||
|
||||
return new Promise( resolve => {
|
||||
|
||||
let scripts = [],
|
||||
scriptsToLoad = 0;
|
||||
|
||||
dependencies.forEach( s => {
|
||||
// Load if there's no condition or the condition is truthy
|
||||
if( !s.condition || s.condition() ) {
|
||||
if( s.async ) {
|
||||
this.asyncDependencies.push( s );
|
||||
}
|
||||
else {
|
||||
scripts.push( s );
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
if( scripts.length ) {
|
||||
scriptsToLoad = scripts.length;
|
||||
|
||||
const scriptLoadedCallback = (s) => {
|
||||
if( s && typeof s.callback === 'function' ) s.callback();
|
||||
|
||||
if( --scriptsToLoad === 0 ) {
|
||||
this.initPlugins().then( resolve );
|
||||
}
|
||||
};
|
||||
|
||||
// Load synchronous scripts
|
||||
scripts.forEach( s => {
|
||||
if( typeof s.id === 'string' ) {
|
||||
this.registerPlugin( s );
|
||||
scriptLoadedCallback( s );
|
||||
}
|
||||
else if( typeof s.src === 'string' ) {
|
||||
loadScript( s.src, () => scriptLoadedCallback(s) );
|
||||
}
|
||||
else {
|
||||
console.warn( 'Unrecognized plugin format', s );
|
||||
scriptLoadedCallback();
|
||||
}
|
||||
} );
|
||||
}
|
||||
else {
|
||||
this.initPlugins().then( resolve );
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes our plugins and waits for them to be ready
|
||||
* before proceeding.
|
||||
*/
|
||||
initPlugins() {
|
||||
|
||||
return new Promise( resolve => {
|
||||
|
||||
let pluginValues = Object.values( this.registeredPlugins );
|
||||
let pluginsToInitialize = pluginValues.length;
|
||||
|
||||
// If there are no plugins, skip this step
|
||||
if( pluginsToInitialize === 0 ) {
|
||||
this.loadAsync().then( resolve );
|
||||
}
|
||||
// ... otherwise initialize plugins
|
||||
else {
|
||||
|
||||
let initNextPlugin;
|
||||
|
||||
let afterPlugInitialized = () => {
|
||||
if( --pluginsToInitialize === 0 ) {
|
||||
this.loadAsync().then( resolve );
|
||||
}
|
||||
else {
|
||||
initNextPlugin();
|
||||
}
|
||||
};
|
||||
|
||||
let i = 0;
|
||||
|
||||
// Initialize plugins serially
|
||||
initNextPlugin = () => {
|
||||
|
||||
let plugin = pluginValues[i++];
|
||||
|
||||
// If the plugin has an 'init' method, invoke it
|
||||
if( typeof plugin.init === 'function' ) {
|
||||
let promise = plugin.init( this.Reveal );
|
||||
|
||||
// If the plugin returned a Promise, wait for it
|
||||
if( promise && typeof promise.then === 'function' ) {
|
||||
promise.then( afterPlugInitialized );
|
||||
}
|
||||
else {
|
||||
afterPlugInitialized();
|
||||
}
|
||||
}
|
||||
else {
|
||||
afterPlugInitialized();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
initNextPlugin();
|
||||
|
||||
}
|
||||
|
||||
} )
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all async reveal.js dependencies.
|
||||
*/
|
||||
loadAsync() {
|
||||
|
||||
this.state = 'loaded';
|
||||
|
||||
if( this.asyncDependencies.length ) {
|
||||
this.asyncDependencies.forEach( s => {
|
||||
loadScript( s.src, s.callback );
|
||||
} );
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new plugin with this reveal.js instance.
|
||||
*
|
||||
* reveal.js waits for all registered plugins to initialize
|
||||
* before considering itself ready, as long as the plugin
|
||||
* is registered before calling `Reveal.initialize()`.
|
||||
*/
|
||||
registerPlugin( plugin ) {
|
||||
|
||||
// Backwards compatibility to make reveal.js ~3.9.0
|
||||
// plugins work with reveal.js 4.0.0
|
||||
if( arguments.length === 2 && typeof arguments[0] === 'string' ) {
|
||||
plugin = arguments[1];
|
||||
plugin.id = arguments[0];
|
||||
}
|
||||
// Plugin can optionally be a function which we call
|
||||
// to create an instance of the plugin
|
||||
else if( typeof plugin === 'function' ) {
|
||||
plugin = plugin();
|
||||
}
|
||||
|
||||
let id = plugin.id;
|
||||
|
||||
if( typeof id !== 'string' ) {
|
||||
console.warn( 'Unrecognized plugin format; can\'t find plugin.id', plugin );
|
||||
}
|
||||
else if( this.registeredPlugins[id] === undefined ) {
|
||||
this.registeredPlugins[id] = plugin;
|
||||
|
||||
// If a plugin is registered after reveal.js is loaded,
|
||||
// initialize it right away
|
||||
if( this.state === 'loaded' && typeof plugin.init === 'function' ) {
|
||||
plugin.init( this.Reveal );
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.warn( 'reveal.js: "'+ id +'" plugin has already been registered' );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a specific plugin has been registered.
|
||||
*
|
||||
* @param {String} id Unique plugin identifier
|
||||
*/
|
||||
hasPlugin( id ) {
|
||||
|
||||
return !!this.registeredPlugins[id];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the specific plugin instance, if a plugin
|
||||
* with the given ID has been registered.
|
||||
*
|
||||
* @param {String} id Unique plugin identifier
|
||||
*/
|
||||
getPlugin( id ) {
|
||||
|
||||
return this.registeredPlugins[id];
|
||||
|
||||
}
|
||||
|
||||
getRegisteredPlugins() {
|
||||
|
||||
return this.registeredPlugins;
|
||||
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
Object.values( this.registeredPlugins ).forEach( plugin => {
|
||||
if( typeof plugin.destroy === 'function' ) {
|
||||
plugin.destroy();
|
||||
}
|
||||
} );
|
||||
|
||||
this.registeredPlugins = {};
|
||||
this.asyncDependencies = [];
|
||||
|
||||
}
|
||||
|
||||
}
|
129
js/controllers/pointer.js
Normal file
129
js/controllers/pointer.js
Normal file
@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Handles hiding of the pointer/cursor when inactive.
|
||||
*/
|
||||
export default class Pointer {
|
||||
|
||||
constructor( Reveal ) {
|
||||
|
||||
this.Reveal = Reveal;
|
||||
|
||||
// Throttles mouse wheel navigation
|
||||
this.lastMouseWheelStep = 0;
|
||||
|
||||
// Is the mouse pointer currently hidden from view
|
||||
this.cursorHidden = false;
|
||||
|
||||
// Timeout used to determine when the cursor is inactive
|
||||
this.cursorInactiveTimeout = 0;
|
||||
|
||||
this.onDocumentCursorActive = this.onDocumentCursorActive.bind( this );
|
||||
this.onDocumentMouseScroll = this.onDocumentMouseScroll.bind( this );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the reveal.js config is updated.
|
||||
*/
|
||||
configure( config, oldConfig ) {
|
||||
|
||||
if( config.mouseWheel ) {
|
||||
document.addEventListener( 'DOMMouseScroll', this.onDocumentMouseScroll, false ); // FF
|
||||
document.addEventListener( 'mousewheel', this.onDocumentMouseScroll, false );
|
||||
}
|
||||
else {
|
||||
document.removeEventListener( 'DOMMouseScroll', this.onDocumentMouseScroll, false ); // FF
|
||||
document.removeEventListener( 'mousewheel', this.onDocumentMouseScroll, false );
|
||||
}
|
||||
|
||||
// Auto-hide the mouse pointer when its inactive
|
||||
if( config.hideInactiveCursor ) {
|
||||
document.addEventListener( 'mousemove', this.onDocumentCursorActive, false );
|
||||
document.addEventListener( 'mousedown', this.onDocumentCursorActive, false );
|
||||
}
|
||||
else {
|
||||
this.showCursor();
|
||||
|
||||
document.removeEventListener( 'mousemove', this.onDocumentCursorActive, false );
|
||||
document.removeEventListener( 'mousedown', this.onDocumentCursorActive, false );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the mouse pointer after it has been hidden with
|
||||
* #hideCursor.
|
||||
*/
|
||||
showCursor() {
|
||||
|
||||
if( this.cursorHidden ) {
|
||||
this.cursorHidden = false;
|
||||
this.Reveal.getRevealElement().style.cursor = '';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the mouse pointer when it's on top of the .reveal
|
||||
* container.
|
||||
*/
|
||||
hideCursor() {
|
||||
|
||||
if( this.cursorHidden === false ) {
|
||||
this.cursorHidden = true;
|
||||
this.Reveal.getRevealElement().style.cursor = 'none';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
this.showCursor();
|
||||
|
||||
document.removeEventListener( 'DOMMouseScroll', this.onDocumentMouseScroll, false );
|
||||
document.removeEventListener( 'mousewheel', this.onDocumentMouseScroll, false );
|
||||
document.removeEventListener( 'mousemove', this.onDocumentCursorActive, false );
|
||||
document.removeEventListener( 'mousedown', this.onDocumentCursorActive, false );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever there is mouse input at the document level
|
||||
* to determine if the cursor is active or not.
|
||||
*
|
||||
* @param {object} event
|
||||
*/
|
||||
onDocumentCursorActive( event ) {
|
||||
|
||||
this.showCursor();
|
||||
|
||||
clearTimeout( this.cursorInactiveTimeout );
|
||||
|
||||
this.cursorInactiveTimeout = setTimeout( this.hideCursor.bind( this ), this.Reveal.getConfig().hideCursorTime );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouse wheel scrolling, throttled to avoid skipping
|
||||
* multiple slides.
|
||||
*
|
||||
* @param {object} event
|
||||
*/
|
||||
onDocumentMouseScroll( event ) {
|
||||
|
||||
if( Date.now() - this.lastMouseWheelStep > 1000 ) {
|
||||
|
||||
this.lastMouseWheelStep = Date.now();
|
||||
|
||||
let delta = event.detail || -event.wheelDelta;
|
||||
if( delta > 0 ) {
|
||||
this.Reveal.next();
|
||||
}
|
||||
else if( delta < 0 ) {
|
||||
this.Reveal.prev();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
237
js/controllers/print.js
Normal file
237
js/controllers/print.js
Normal file
@ -0,0 +1,237 @@
|
||||
import { SLIDES_SELECTOR } from '../utils/constants.js'
|
||||
import { queryAll, createStyleSheet } from '../utils/util.js'
|
||||
|
||||
/**
|
||||
* Setups up our presentation for printing/exporting to PDF.
|
||||
*/
|
||||
export default class Print {
|
||||
|
||||
constructor( Reveal ) {
|
||||
|
||||
this.Reveal = Reveal;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the presentation for printing to a static
|
||||
* PDF.
|
||||
*/
|
||||
async setupPDF() {
|
||||
|
||||
const config = this.Reveal.getConfig();
|
||||
const slides = queryAll( this.Reveal.getRevealElement(), SLIDES_SELECTOR )
|
||||
|
||||
// Compute slide numbers now, before we start duplicating slides
|
||||
const injectPageNumbers = config.slideNumber && /all|print/i.test( config.showSlideNumber );
|
||||
|
||||
const slideSize = this.Reveal.getComputedSlideSize( window.innerWidth, window.innerHeight );
|
||||
|
||||
// Dimensions of the PDF pages
|
||||
const pageWidth = Math.floor( slideSize.width * ( 1 + config.margin ) ),
|
||||
pageHeight = Math.floor( slideSize.height * ( 1 + config.margin ) );
|
||||
|
||||
// Dimensions of slides within the pages
|
||||
const slideWidth = slideSize.width,
|
||||
slideHeight = slideSize.height;
|
||||
|
||||
await new Promise( requestAnimationFrame );
|
||||
|
||||
// Let the browser know what page size we want to print
|
||||
createStyleSheet( '@page{size:'+ pageWidth +'px '+ pageHeight +'px; margin: 0px;}' );
|
||||
|
||||
// Limit the size of certain elements to the dimensions of the slide
|
||||
createStyleSheet( '.reveal section>img, .reveal section>video, .reveal section>iframe{max-width: '+ slideWidth +'px; max-height:'+ slideHeight +'px}' );
|
||||
|
||||
document.documentElement.classList.add( 'print-pdf' );
|
||||
document.body.style.width = pageWidth + 'px';
|
||||
document.body.style.height = pageHeight + 'px';
|
||||
|
||||
const viewportElement = document.querySelector( '.reveal-viewport' );
|
||||
let presentationBackground;
|
||||
if( viewportElement ) {
|
||||
const viewportStyles = window.getComputedStyle( viewportElement );
|
||||
if( viewportStyles && viewportStyles.background ) {
|
||||
presentationBackground = viewportStyles.background;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure stretch elements fit on slide
|
||||
await new Promise( requestAnimationFrame );
|
||||
this.Reveal.layoutSlideContents( slideWidth, slideHeight );
|
||||
|
||||
// Batch scrollHeight access to prevent layout thrashing
|
||||
await new Promise( requestAnimationFrame );
|
||||
|
||||
const slideScrollHeights = slides.map( slide => slide.scrollHeight );
|
||||
|
||||
const pages = [];
|
||||
const pageContainer = slides[0].parentNode;
|
||||
let slideNumber = 1;
|
||||
|
||||
// Slide and slide background layout
|
||||
slides.forEach( function( slide, index ) {
|
||||
|
||||
// Vertical stacks are not centred since their section
|
||||
// children will be
|
||||
if( slide.classList.contains( 'stack' ) === false ) {
|
||||
// Center the slide inside of the page, giving the slide some margin
|
||||
let left = ( pageWidth - slideWidth ) / 2;
|
||||
let top = ( pageHeight - slideHeight ) / 2;
|
||||
|
||||
const contentHeight = slideScrollHeights[ index ];
|
||||
let numberOfPages = Math.max( Math.ceil( contentHeight / pageHeight ), 1 );
|
||||
|
||||
// Adhere to configured pages per slide limit
|
||||
numberOfPages = Math.min( numberOfPages, config.pdfMaxPagesPerSlide );
|
||||
|
||||
// Center slides vertically
|
||||
if( numberOfPages === 1 && config.center || slide.classList.contains( 'center' ) ) {
|
||||
top = Math.max( ( pageHeight - contentHeight ) / 2, 0 );
|
||||
}
|
||||
|
||||
// Wrap the slide in a page element and hide its overflow
|
||||
// so that no page ever flows onto another
|
||||
const page = document.createElement( 'div' );
|
||||
pages.push( page );
|
||||
|
||||
page.className = 'pdf-page';
|
||||
page.style.height = ( ( pageHeight + config.pdfPageHeightOffset ) * numberOfPages ) + 'px';
|
||||
|
||||
// Copy the presentation-wide background to each individual
|
||||
// page when printing
|
||||
if( presentationBackground ) {
|
||||
page.style.background = presentationBackground;
|
||||
}
|
||||
|
||||
page.appendChild( slide );
|
||||
|
||||
// Position the slide inside of the page
|
||||
slide.style.left = left + 'px';
|
||||
slide.style.top = top + 'px';
|
||||
slide.style.width = slideWidth + 'px';
|
||||
|
||||
this.Reveal.slideContent.layout( slide );
|
||||
|
||||
if( slide.slideBackgroundElement ) {
|
||||
page.insertBefore( slide.slideBackgroundElement, slide );
|
||||
}
|
||||
|
||||
// Inject notes if `showNotes` is enabled
|
||||
if( config.showNotes ) {
|
||||
|
||||
// Are there notes for this slide?
|
||||
const notes = this.Reveal.getSlideNotes( slide );
|
||||
if( notes ) {
|
||||
|
||||
const notesSpacing = 8;
|
||||
const notesLayout = typeof config.showNotes === 'string' ? config.showNotes : 'inline';
|
||||
const notesElement = document.createElement( 'div' );
|
||||
notesElement.classList.add( 'speaker-notes' );
|
||||
notesElement.classList.add( 'speaker-notes-pdf' );
|
||||
notesElement.setAttribute( 'data-layout', notesLayout );
|
||||
notesElement.innerHTML = notes;
|
||||
|
||||
if( notesLayout === 'separate-page' ) {
|
||||
pages.push( notesElement );
|
||||
}
|
||||
else {
|
||||
notesElement.style.left = notesSpacing + 'px';
|
||||
notesElement.style.bottom = notesSpacing + 'px';
|
||||
notesElement.style.width = ( pageWidth - notesSpacing*2 ) + 'px';
|
||||
page.appendChild( notesElement );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Inject page numbers if `slideNumbers` are enabled
|
||||
if( injectPageNumbers ) {
|
||||
const numberElement = document.createElement( 'div' );
|
||||
numberElement.classList.add( 'slide-number' );
|
||||
numberElement.classList.add( 'slide-number-pdf' );
|
||||
numberElement.innerHTML = slideNumber++;
|
||||
page.appendChild( numberElement );
|
||||
}
|
||||
|
||||
// Copy page and show fragments one after another
|
||||
if( config.pdfSeparateFragments ) {
|
||||
|
||||
// Each fragment 'group' is an array containing one or more
|
||||
// fragments. Multiple fragments that appear at the same time
|
||||
// are part of the same group.
|
||||
const fragmentGroups = this.Reveal.fragments.sort( page.querySelectorAll( '.fragment' ), true );
|
||||
|
||||
let previousFragmentStep;
|
||||
|
||||
fragmentGroups.forEach( function( fragments, index ) {
|
||||
|
||||
// Remove 'current-fragment' from the previous group
|
||||
if( previousFragmentStep ) {
|
||||
previousFragmentStep.forEach( function( fragment ) {
|
||||
fragment.classList.remove( 'current-fragment' );
|
||||
} );
|
||||
}
|
||||
|
||||
// Show the fragments for the current index
|
||||
fragments.forEach( function( fragment ) {
|
||||
fragment.classList.add( 'visible', 'current-fragment' );
|
||||
}, this );
|
||||
|
||||
// Create a separate page for the current fragment state
|
||||
const clonedPage = page.cloneNode( true );
|
||||
|
||||
// Inject unique page numbers for fragments
|
||||
if( injectPageNumbers ) {
|
||||
const numberElement = clonedPage.querySelector( '.slide-number-pdf' );
|
||||
const fragmentNumber = index + 1;
|
||||
numberElement.innerHTML += '.' + fragmentNumber;
|
||||
}
|
||||
|
||||
pages.push( clonedPage );
|
||||
|
||||
previousFragmentStep = fragments;
|
||||
|
||||
}, this );
|
||||
|
||||
// Reset the first/original page so that all fragments are hidden
|
||||
fragmentGroups.forEach( function( fragments ) {
|
||||
fragments.forEach( function( fragment ) {
|
||||
fragment.classList.remove( 'visible', 'current-fragment' );
|
||||
} );
|
||||
} );
|
||||
|
||||
}
|
||||
// Show all fragments
|
||||
else {
|
||||
queryAll( page, '.fragment:not(.fade-out)' ).forEach( function( fragment ) {
|
||||
fragment.classList.add( 'visible' );
|
||||
} );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}, this );
|
||||
|
||||
await new Promise( requestAnimationFrame );
|
||||
|
||||
pages.forEach( page => pageContainer.appendChild( page ) );
|
||||
|
||||
// Re-run JS-based content layout after the slide is added to page DOM
|
||||
this.Reveal.slideContent.layout( this.Reveal.getSlidesElement() );
|
||||
|
||||
// Notify subscribers that the PDF layout is good to go
|
||||
this.Reveal.dispatchEvent({ type: 'pdf-ready' });
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this instance is being used to print a PDF.
|
||||
*/
|
||||
isPrintingPDF() {
|
||||
|
||||
return ( /print-pdf/gi ).test( window.location.search );
|
||||
|
||||
}
|
||||
|
||||
}
|
110
js/controllers/progress.js
Normal file
110
js/controllers/progress.js
Normal file
@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Creates a visual progress bar for the presentation.
|
||||
*/
|
||||
export default class Progress {
|
||||
|
||||
constructor( Reveal ) {
|
||||
|
||||
this.Reveal = Reveal;
|
||||
|
||||
this.onProgressClicked = this.onProgressClicked.bind( this );
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
this.element = document.createElement( 'div' );
|
||||
this.element.className = 'progress';
|
||||
this.Reveal.getRevealElement().appendChild( this.element );
|
||||
|
||||
this.bar = document.createElement( 'span' );
|
||||
this.element.appendChild( this.bar );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the reveal.js config is updated.
|
||||
*/
|
||||
configure( config, oldConfig ) {
|
||||
|
||||
this.element.style.display = config.progress ? 'block' : 'none';
|
||||
|
||||
}
|
||||
|
||||
bind() {
|
||||
|
||||
if( this.Reveal.getConfig().progress && this.element ) {
|
||||
this.element.addEventListener( 'click', this.onProgressClicked, false );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
unbind() {
|
||||
|
||||
if ( this.Reveal.getConfig().progress && this.element ) {
|
||||
this.element.removeEventListener( 'click', this.onProgressClicked, false );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the progress bar to reflect the current slide.
|
||||
*/
|
||||
update() {
|
||||
|
||||
// Update progress if enabled
|
||||
if( this.Reveal.getConfig().progress && this.bar ) {
|
||||
|
||||
let scale = this.Reveal.getProgress();
|
||||
|
||||
// Don't fill the progress bar if there's only one slide
|
||||
if( this.Reveal.getTotalSlides() < 2 ) {
|
||||
scale = 0;
|
||||
}
|
||||
|
||||
this.bar.style.transform = 'scaleX('+ scale +')';
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getMaxWidth() {
|
||||
|
||||
return this.Reveal.getRevealElement().offsetWidth;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicking on the progress bar results in a navigation to the
|
||||
* closest approximate horizontal slide using this equation:
|
||||
*
|
||||
* ( clickX / presentationWidth ) * numberOfSlides
|
||||
*
|
||||
* @param {object} event
|
||||
*/
|
||||
onProgressClicked( event ) {
|
||||
|
||||
this.Reveal.onUserInput( event );
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
let slides = this.Reveal.getSlides();
|
||||
let slidesTotal = slides.length;
|
||||
let slideIndex = Math.floor( ( event.clientX / this.getMaxWidth() ) * slidesTotal );
|
||||
|
||||
if( this.Reveal.getConfig().rtl ) {
|
||||
slideIndex = slidesTotal - slideIndex;
|
||||
}
|
||||
|
||||
let targetIndices = this.Reveal.getIndices(slides[slideIndex]);
|
||||
this.Reveal.slide( targetIndices.h, targetIndices.v );
|
||||
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
this.element.remove();
|
||||
|
||||
}
|
||||
|
||||
}
|
480
js/controllers/slidecontent.js
Normal file
480
js/controllers/slidecontent.js
Normal file
@ -0,0 +1,480 @@
|
||||
import { extend, queryAll, closest, getMimeTypeFromFile, encodeRFC3986URI } from '../utils/util.js'
|
||||
import { isMobile } from '../utils/device.js'
|
||||
|
||||
import fitty from 'fitty';
|
||||
|
||||
/**
|
||||
* Handles loading, unloading and playback of slide
|
||||
* content such as images, videos and iframes.
|
||||
*/
|
||||
export default class SlideContent {
|
||||
|
||||
constructor( Reveal ) {
|
||||
|
||||
this.Reveal = Reveal;
|
||||
|
||||
this.startEmbeddedIframe = this.startEmbeddedIframe.bind( this );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the given element be preloaded?
|
||||
* Decides based on local element attributes and global config.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
*/
|
||||
shouldPreload( element ) {
|
||||
|
||||
// Prefer an explicit global preload setting
|
||||
let preload = this.Reveal.getConfig().preloadIframes;
|
||||
|
||||
// If no global setting is available, fall back on the element's
|
||||
// own preload setting
|
||||
if( typeof preload !== 'boolean' ) {
|
||||
preload = element.hasAttribute( 'data-preload' );
|
||||
}
|
||||
|
||||
return preload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the given slide is within the configured view
|
||||
* distance. Shows the slide element and loads any content
|
||||
* that is set to load lazily (data-src).
|
||||
*
|
||||
* @param {HTMLElement} slide Slide to show
|
||||
*/
|
||||
load( slide, options = {} ) {
|
||||
|
||||
// Show the slide element
|
||||
slide.style.display = this.Reveal.getConfig().display;
|
||||
|
||||
// Media elements with data-src attributes
|
||||
queryAll( slide, 'img[data-src], video[data-src], audio[data-src], iframe[data-src]' ).forEach( element => {
|
||||
if( element.tagName !== 'IFRAME' || this.shouldPreload( element ) ) {
|
||||
element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
|
||||
element.setAttribute( 'data-lazy-loaded', '' );
|
||||
element.removeAttribute( 'data-src' );
|
||||
}
|
||||
} );
|
||||
|
||||
// Media elements with <source> children
|
||||
queryAll( slide, 'video, audio' ).forEach( media => {
|
||||
let sources = 0;
|
||||
|
||||
queryAll( media, 'source[data-src]' ).forEach( source => {
|
||||
source.setAttribute( 'src', source.getAttribute( 'data-src' ) );
|
||||
source.removeAttribute( 'data-src' );
|
||||
source.setAttribute( 'data-lazy-loaded', '' );
|
||||
sources += 1;
|
||||
} );
|
||||
|
||||
// Enable inline video playback in mobile Safari
|
||||
if( isMobile && media.tagName === 'VIDEO' ) {
|
||||
media.setAttribute( 'playsinline', '' );
|
||||
}
|
||||
|
||||
// If we rewrote sources for this video/audio element, we need
|
||||
// to manually tell it to load from its new origin
|
||||
if( sources > 0 ) {
|
||||
media.load();
|
||||
}
|
||||
} );
|
||||
|
||||
|
||||
// Show the corresponding background element
|
||||
let background = slide.slideBackgroundElement;
|
||||
if( background ) {
|
||||
background.style.display = 'block';
|
||||
|
||||
let backgroundContent = slide.slideBackgroundContentElement;
|
||||
let backgroundIframe = slide.getAttribute( 'data-background-iframe' );
|
||||
|
||||
// If the background contains media, load it
|
||||
if( background.hasAttribute( 'data-loaded' ) === false ) {
|
||||
background.setAttribute( 'data-loaded', 'true' );
|
||||
|
||||
let backgroundImage = slide.getAttribute( 'data-background-image' ),
|
||||
backgroundVideo = slide.getAttribute( 'data-background-video' ),
|
||||
backgroundVideoLoop = slide.hasAttribute( 'data-background-video-loop' ),
|
||||
backgroundVideoMuted = slide.hasAttribute( 'data-background-video-muted' );
|
||||
|
||||
// Images
|
||||
if( backgroundImage ) {
|
||||
// base64
|
||||
if( /^data:/.test( backgroundImage.trim() ) ) {
|
||||
backgroundContent.style.backgroundImage = `url(${backgroundImage.trim()})`;
|
||||
}
|
||||
// URL(s)
|
||||
else {
|
||||
backgroundContent.style.backgroundImage = backgroundImage.split( ',' ).map( background => {
|
||||
// Decode URL(s) that are already encoded first
|
||||
let decoded = decodeURI(background.trim());
|
||||
return `url(${encodeRFC3986URI(decoded)})`;
|
||||
}).join( ',' );
|
||||
}
|
||||
}
|
||||
// Videos
|
||||
else if ( backgroundVideo && !this.Reveal.isSpeakerNotes() ) {
|
||||
let video = document.createElement( 'video' );
|
||||
|
||||
if( backgroundVideoLoop ) {
|
||||
video.setAttribute( 'loop', '' );
|
||||
}
|
||||
|
||||
if( backgroundVideoMuted ) {
|
||||
video.muted = true;
|
||||
}
|
||||
|
||||
// Enable inline playback in mobile Safari
|
||||
//
|
||||
// Mute is required for video to play when using
|
||||
// swipe gestures to navigate since they don't
|
||||
// count as direct user actions :'(
|
||||
if( isMobile ) {
|
||||
video.muted = true;
|
||||
video.setAttribute( 'playsinline', '' );
|
||||
}
|
||||
|
||||
// Support comma separated lists of video sources
|
||||
backgroundVideo.split( ',' ).forEach( source => {
|
||||
let type = getMimeTypeFromFile( source );
|
||||
if( type ) {
|
||||
video.innerHTML += `<source src="${source}" type="${type}">`;
|
||||
}
|
||||
else {
|
||||
video.innerHTML += `<source src="${source}">`;
|
||||
}
|
||||
} );
|
||||
|
||||
backgroundContent.appendChild( video );
|
||||
}
|
||||
// Iframes
|
||||
else if( backgroundIframe && options.excludeIframes !== true ) {
|
||||
let iframe = document.createElement( 'iframe' );
|
||||
iframe.setAttribute( 'allowfullscreen', '' );
|
||||
iframe.setAttribute( 'mozallowfullscreen', '' );
|
||||
iframe.setAttribute( 'webkitallowfullscreen', '' );
|
||||
iframe.setAttribute( 'allow', 'autoplay' );
|
||||
|
||||
iframe.setAttribute( 'data-src', backgroundIframe );
|
||||
|
||||
iframe.style.width = '100%';
|
||||
iframe.style.height = '100%';
|
||||
iframe.style.maxHeight = '100%';
|
||||
iframe.style.maxWidth = '100%';
|
||||
|
||||
backgroundContent.appendChild( iframe );
|
||||
}
|
||||
}
|
||||
|
||||
// Start loading preloadable iframes
|
||||
let backgroundIframeElement = backgroundContent.querySelector( 'iframe[data-src]' );
|
||||
if( backgroundIframeElement ) {
|
||||
|
||||
// Check if this iframe is eligible to be preloaded
|
||||
if( this.shouldPreload( background ) && !/autoplay=(1|true|yes)/gi.test( backgroundIframe ) ) {
|
||||
if( backgroundIframeElement.getAttribute( 'src' ) !== backgroundIframe ) {
|
||||
backgroundIframeElement.setAttribute( 'src', backgroundIframe );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.layout( slide );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies JS-dependent layout helpers for the scope.
|
||||
*/
|
||||
layout( scopeElement ) {
|
||||
|
||||
// Autosize text with the r-fit-text class based on the
|
||||
// size of its container. This needs to happen after the
|
||||
// slide is visible in order to measure the text.
|
||||
Array.from( scopeElement.querySelectorAll( '.r-fit-text' ) ).forEach( element => {
|
||||
fitty( element, {
|
||||
minSize: 24,
|
||||
maxSize: this.Reveal.getConfig().height * 0.8,
|
||||
observeMutations: false,
|
||||
observeWindow: false
|
||||
} );
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Unloads and hides the given slide. This is called when the
|
||||
* slide is moved outside of the configured view distance.
|
||||
*
|
||||
* @param {HTMLElement} slide
|
||||
*/
|
||||
unload( slide ) {
|
||||
|
||||
// Hide the slide element
|
||||
slide.style.display = 'none';
|
||||
|
||||
// Hide the corresponding background element
|
||||
let background = this.Reveal.getSlideBackground( slide );
|
||||
if( background ) {
|
||||
background.style.display = 'none';
|
||||
|
||||
// Unload any background iframes
|
||||
queryAll( background, 'iframe[src]' ).forEach( element => {
|
||||
element.removeAttribute( 'src' );
|
||||
} );
|
||||
}
|
||||
|
||||
// Reset lazy-loaded media elements with src attributes
|
||||
queryAll( slide, 'video[data-lazy-loaded][src], audio[data-lazy-loaded][src], iframe[data-lazy-loaded][src]' ).forEach( element => {
|
||||
element.setAttribute( 'data-src', element.getAttribute( 'src' ) );
|
||||
element.removeAttribute( 'src' );
|
||||
} );
|
||||
|
||||
// Reset lazy-loaded media elements with <source> children
|
||||
queryAll( slide, 'video[data-lazy-loaded] source[src], audio source[src]' ).forEach( source => {
|
||||
source.setAttribute( 'data-src', source.getAttribute( 'src' ) );
|
||||
source.removeAttribute( 'src' );
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforces origin-specific format rules for embedded media.
|
||||
*/
|
||||
formatEmbeddedContent() {
|
||||
|
||||
let _appendParamToIframeSource = ( sourceAttribute, sourceURL, param ) => {
|
||||
queryAll( this.Reveal.getSlidesElement(), 'iframe['+ sourceAttribute +'*="'+ sourceURL +'"]' ).forEach( el => {
|
||||
let src = el.getAttribute( sourceAttribute );
|
||||
if( src && src.indexOf( param ) === -1 ) {
|
||||
el.setAttribute( sourceAttribute, src + ( !/\?/.test( src ) ? '?' : '&' ) + param );
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// YouTube frames must include "?enablejsapi=1"
|
||||
_appendParamToIframeSource( 'src', 'youtube.com/embed/', 'enablejsapi=1' );
|
||||
_appendParamToIframeSource( 'data-src', 'youtube.com/embed/', 'enablejsapi=1' );
|
||||
|
||||
// Vimeo frames must include "?api=1"
|
||||
_appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' );
|
||||
_appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Start playback of any embedded content inside of
|
||||
* the given element.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
*/
|
||||
startEmbeddedContent( element ) {
|
||||
|
||||
if( element && !this.Reveal.isSpeakerNotes() ) {
|
||||
|
||||
// Restart GIFs
|
||||
queryAll( element, 'img[src$=".gif"]' ).forEach( el => {
|
||||
// Setting the same unchanged source like this was confirmed
|
||||
// to work in Chrome, FF & Safari
|
||||
el.setAttribute( 'src', el.getAttribute( 'src' ) );
|
||||
} );
|
||||
|
||||
// HTML5 media elements
|
||||
queryAll( element, 'video, audio' ).forEach( el => {
|
||||
if( closest( el, '.fragment' ) && !closest( el, '.fragment.visible' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer an explicit global autoplay setting
|
||||
let autoplay = this.Reveal.getConfig().autoPlayMedia;
|
||||
|
||||
// If no global setting is available, fall back on the element's
|
||||
// own autoplay setting
|
||||
if( typeof autoplay !== 'boolean' ) {
|
||||
autoplay = el.hasAttribute( 'data-autoplay' ) || !!closest( el, '.slide-background' );
|
||||
}
|
||||
|
||||
if( autoplay && typeof el.play === 'function' ) {
|
||||
|
||||
// If the media is ready, start playback
|
||||
if( el.readyState > 1 ) {
|
||||
this.startEmbeddedMedia( { target: el } );
|
||||
}
|
||||
// Mobile devices never fire a loaded event so instead
|
||||
// of waiting, we initiate playback
|
||||
else if( isMobile ) {
|
||||
let promise = el.play();
|
||||
|
||||
// If autoplay does not work, ensure that the controls are visible so
|
||||
// that the viewer can start the media on their own
|
||||
if( promise && typeof promise.catch === 'function' && el.controls === false ) {
|
||||
promise.catch( () => {
|
||||
el.controls = true;
|
||||
|
||||
// Once the video does start playing, hide the controls again
|
||||
el.addEventListener( 'play', () => {
|
||||
el.controls = false;
|
||||
} );
|
||||
} );
|
||||
}
|
||||
}
|
||||
// If the media isn't loaded, wait before playing
|
||||
else {
|
||||
el.removeEventListener( 'loadeddata', this.startEmbeddedMedia ); // remove first to avoid dupes
|
||||
el.addEventListener( 'loadeddata', this.startEmbeddedMedia );
|
||||
}
|
||||
|
||||
}
|
||||
} );
|
||||
|
||||
// Normal iframes
|
||||
queryAll( element, 'iframe[src]' ).forEach( el => {
|
||||
if( closest( el, '.fragment' ) && !closest( el, '.fragment.visible' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.startEmbeddedIframe( { target: el } );
|
||||
} );
|
||||
|
||||
// Lazy loading iframes
|
||||
queryAll( element, 'iframe[data-src]' ).forEach( el => {
|
||||
if( closest( el, '.fragment' ) && !closest( el, '.fragment.visible' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if( el.getAttribute( 'src' ) !== el.getAttribute( 'data-src' ) ) {
|
||||
el.removeEventListener( 'load', this.startEmbeddedIframe ); // remove first to avoid dupes
|
||||
el.addEventListener( 'load', this.startEmbeddedIframe );
|
||||
el.setAttribute( 'src', el.getAttribute( 'data-src' ) );
|
||||
}
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts playing an embedded video/audio element after
|
||||
* it has finished loading.
|
||||
*
|
||||
* @param {object} event
|
||||
*/
|
||||
startEmbeddedMedia( event ) {
|
||||
|
||||
let isAttachedToDOM = !!closest( event.target, 'html' ),
|
||||
isVisible = !!closest( event.target, '.present' );
|
||||
|
||||
if( isAttachedToDOM && isVisible ) {
|
||||
event.target.currentTime = 0;
|
||||
event.target.play();
|
||||
}
|
||||
|
||||
event.target.removeEventListener( 'loadeddata', this.startEmbeddedMedia );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* "Starts" the content of an embedded iframe using the
|
||||
* postMessage API.
|
||||
*
|
||||
* @param {object} event
|
||||
*/
|
||||
startEmbeddedIframe( event ) {
|
||||
|
||||
let iframe = event.target;
|
||||
|
||||
if( iframe && iframe.contentWindow ) {
|
||||
|
||||
let isAttachedToDOM = !!closest( event.target, 'html' ),
|
||||
isVisible = !!closest( event.target, '.present' );
|
||||
|
||||
if( isAttachedToDOM && isVisible ) {
|
||||
|
||||
// Prefer an explicit global autoplay setting
|
||||
let autoplay = this.Reveal.getConfig().autoPlayMedia;
|
||||
|
||||
// If no global setting is available, fall back on the element's
|
||||
// own autoplay setting
|
||||
if( typeof autoplay !== 'boolean' ) {
|
||||
autoplay = iframe.hasAttribute( 'data-autoplay' ) || !!closest( iframe, '.slide-background' );
|
||||
}
|
||||
|
||||
// YouTube postMessage API
|
||||
if( /youtube\.com\/embed\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {
|
||||
iframe.contentWindow.postMessage( '{"event":"command","func":"playVideo","args":""}', '*' );
|
||||
}
|
||||
// Vimeo postMessage API
|
||||
else if( /player\.vimeo\.com\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {
|
||||
iframe.contentWindow.postMessage( '{"method":"play"}', '*' );
|
||||
}
|
||||
// Generic postMessage API
|
||||
else {
|
||||
iframe.contentWindow.postMessage( 'slide:start', '*' );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop playback of any embedded content inside of
|
||||
* the targeted slide.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
*/
|
||||
stopEmbeddedContent( element, options = {} ) {
|
||||
|
||||
options = extend( {
|
||||
// Defaults
|
||||
unloadIframes: true
|
||||
}, options );
|
||||
|
||||
if( element && element.parentNode ) {
|
||||
// HTML5 media elements
|
||||
queryAll( element, 'video, audio' ).forEach( el => {
|
||||
if( !el.hasAttribute( 'data-ignore' ) && typeof el.pause === 'function' ) {
|
||||
el.setAttribute('data-paused-by-reveal', '');
|
||||
el.pause();
|
||||
}
|
||||
} );
|
||||
|
||||
// Generic postMessage API for non-lazy loaded iframes
|
||||
queryAll( element, 'iframe' ).forEach( el => {
|
||||
if( el.contentWindow ) el.contentWindow.postMessage( 'slide:stop', '*' );
|
||||
el.removeEventListener( 'load', this.startEmbeddedIframe );
|
||||
});
|
||||
|
||||
// YouTube postMessage API
|
||||
queryAll( element, 'iframe[src*="youtube.com/embed/"]' ).forEach( el => {
|
||||
if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {
|
||||
el.contentWindow.postMessage( '{"event":"command","func":"pauseVideo","args":""}', '*' );
|
||||
}
|
||||
});
|
||||
|
||||
// Vimeo postMessage API
|
||||
queryAll( element, 'iframe[src*="player.vimeo.com/"]' ).forEach( el => {
|
||||
if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {
|
||||
el.contentWindow.postMessage( '{"method":"pause"}', '*' );
|
||||
}
|
||||
});
|
||||
|
||||
if( options.unloadIframes === true ) {
|
||||
// Unload lazy-loaded iframes
|
||||
queryAll( element, 'iframe[data-src]' ).forEach( el => {
|
||||
// Only removing the src doesn't actually unload the frame
|
||||
// in all browsers (Firefox) so we set it to blank first
|
||||
el.setAttribute( 'src', 'about:blank' );
|
||||
el.removeAttribute( 'src' );
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
132
js/controllers/slidenumber.js
Normal file
132
js/controllers/slidenumber.js
Normal file
@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Handles the display of reveal.js' optional slide number.
|
||||
*/
|
||||
export default class SlideNumber {
|
||||
|
||||
constructor( Reveal ) {
|
||||
|
||||
this.Reveal = Reveal;
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
this.element = document.createElement( 'div' );
|
||||
this.element.className = 'slide-number';
|
||||
this.Reveal.getRevealElement().appendChild( this.element );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the reveal.js config is updated.
|
||||
*/
|
||||
configure( config, oldConfig ) {
|
||||
|
||||
let slideNumberDisplay = 'none';
|
||||
if( config.slideNumber && !this.Reveal.isPrintingPDF() ) {
|
||||
if( config.showSlideNumber === 'all' ) {
|
||||
slideNumberDisplay = 'block';
|
||||
}
|
||||
else if( config.showSlideNumber === 'speaker' && this.Reveal.isSpeakerNotes() ) {
|
||||
slideNumberDisplay = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
this.element.style.display = slideNumberDisplay;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the slide number to match the current slide.
|
||||
*/
|
||||
update() {
|
||||
|
||||
// Update slide number if enabled
|
||||
if( this.Reveal.getConfig().slideNumber && this.element ) {
|
||||
this.element.innerHTML = this.getSlideNumber();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTML string corresponding to the current slide
|
||||
* number, including formatting.
|
||||
*/
|
||||
getSlideNumber( slide = this.Reveal.getCurrentSlide() ) {
|
||||
|
||||
let config = this.Reveal.getConfig();
|
||||
let value;
|
||||
let format = 'h.v';
|
||||
|
||||
if ( typeof config.slideNumber === 'function' ) {
|
||||
value = config.slideNumber( slide );
|
||||
} else {
|
||||
// Check if a custom number format is available
|
||||
if( typeof config.slideNumber === 'string' ) {
|
||||
format = config.slideNumber;
|
||||
}
|
||||
|
||||
// If there are ONLY vertical slides in this deck, always use
|
||||
// a flattened slide number
|
||||
if( !/c/.test( format ) && this.Reveal.getHorizontalSlides().length === 1 ) {
|
||||
format = 'c';
|
||||
}
|
||||
|
||||
// Offset the current slide number by 1 to make it 1-indexed
|
||||
let horizontalOffset = slide && slide.dataset.visibility === 'uncounted' ? 0 : 1;
|
||||
|
||||
value = [];
|
||||
switch( format ) {
|
||||
case 'c':
|
||||
value.push( this.Reveal.getSlidePastCount( slide ) + horizontalOffset );
|
||||
break;
|
||||
case 'c/t':
|
||||
value.push( this.Reveal.getSlidePastCount( slide ) + horizontalOffset, '/', this.Reveal.getTotalSlides() );
|
||||
break;
|
||||
default:
|
||||
let indices = this.Reveal.getIndices( slide );
|
||||
value.push( indices.h + horizontalOffset );
|
||||
let sep = format === 'h/v' ? '/' : '.';
|
||||
if( this.Reveal.isVerticalSlide( slide ) ) value.push( sep, indices.v + 1 );
|
||||
}
|
||||
}
|
||||
|
||||
let url = '#' + this.Reveal.location.getHash( slide );
|
||||
return this.formatNumber( value[0], value[1], value[2], url );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies HTML formatting to a slide number before it's
|
||||
* written to the DOM.
|
||||
*
|
||||
* @param {number} a Current slide
|
||||
* @param {string} delimiter Character to separate slide numbers
|
||||
* @param {(number|*)} b Total slides
|
||||
* @param {HTMLElement} [url='#'+locationHash()] The url to link to
|
||||
* @return {string} HTML string fragment
|
||||
*/
|
||||
formatNumber( a, delimiter, b, url = '#' + this.Reveal.location.getHash() ) {
|
||||
|
||||
if( typeof b === 'number' && !isNaN( b ) ) {
|
||||
return `<a href="${url}">
|
||||
<span class="slide-number-a">${a}</span>
|
||||
<span class="slide-number-delimiter">${delimiter}</span>
|
||||
<span class="slide-number-b">${b}</span>
|
||||
</a>`;
|
||||
}
|
||||
else {
|
||||
return `<a href="${url}">
|
||||
<span class="slide-number-a">${a}</span>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
this.element.remove();
|
||||
|
||||
}
|
||||
|
||||
}
|
263
js/controllers/touch.js
Normal file
263
js/controllers/touch.js
Normal file
@ -0,0 +1,263 @@
|
||||
import { isAndroid } from '../utils/device.js'
|
||||
import { matches } from '../utils/util.js'
|
||||
|
||||
const SWIPE_THRESHOLD = 40;
|
||||
|
||||
/**
|
||||
* Controls all touch interactions and navigations for
|
||||
* a presentation.
|
||||
*/
|
||||
export default class Touch {
|
||||
|
||||
constructor( Reveal ) {
|
||||
|
||||
this.Reveal = Reveal;
|
||||
|
||||
// Holds information about the currently ongoing touch interaction
|
||||
this.touchStartX = 0;
|
||||
this.touchStartY = 0;
|
||||
this.touchStartCount = 0;
|
||||
this.touchCaptured = false;
|
||||
|
||||
this.onPointerDown = this.onPointerDown.bind( this );
|
||||
this.onPointerMove = this.onPointerMove.bind( this );
|
||||
this.onPointerUp = this.onPointerUp.bind( this );
|
||||
this.onTouchStart = this.onTouchStart.bind( this );
|
||||
this.onTouchMove = this.onTouchMove.bind( this );
|
||||
this.onTouchEnd = this.onTouchEnd.bind( this );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
bind() {
|
||||
|
||||
let revealElement = this.Reveal.getRevealElement();
|
||||
|
||||
if( 'onpointerdown' in window ) {
|
||||
// Use W3C pointer events
|
||||
revealElement.addEventListener( 'pointerdown', this.onPointerDown, false );
|
||||
revealElement.addEventListener( 'pointermove', this.onPointerMove, false );
|
||||
revealElement.addEventListener( 'pointerup', this.onPointerUp, false );
|
||||
}
|
||||
else if( window.navigator.msPointerEnabled ) {
|
||||
// IE 10 uses prefixed version of pointer events
|
||||
revealElement.addEventListener( 'MSPointerDown', this.onPointerDown, false );
|
||||
revealElement.addEventListener( 'MSPointerMove', this.onPointerMove, false );
|
||||
revealElement.addEventListener( 'MSPointerUp', this.onPointerUp, false );
|
||||
}
|
||||
else {
|
||||
// Fall back to touch events
|
||||
revealElement.addEventListener( 'touchstart', this.onTouchStart, false );
|
||||
revealElement.addEventListener( 'touchmove', this.onTouchMove, false );
|
||||
revealElement.addEventListener( 'touchend', this.onTouchEnd, false );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
unbind() {
|
||||
|
||||
let revealElement = this.Reveal.getRevealElement();
|
||||
|
||||
revealElement.removeEventListener( 'pointerdown', this.onPointerDown, false );
|
||||
revealElement.removeEventListener( 'pointermove', this.onPointerMove, false );
|
||||
revealElement.removeEventListener( 'pointerup', this.onPointerUp, false );
|
||||
|
||||
revealElement.removeEventListener( 'MSPointerDown', this.onPointerDown, false );
|
||||
revealElement.removeEventListener( 'MSPointerMove', this.onPointerMove, false );
|
||||
revealElement.removeEventListener( 'MSPointerUp', this.onPointerUp, false );
|
||||
|
||||
revealElement.removeEventListener( 'touchstart', this.onTouchStart, false );
|
||||
revealElement.removeEventListener( 'touchmove', this.onTouchMove, false );
|
||||
revealElement.removeEventListener( 'touchend', this.onTouchEnd, false );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the target element prevents the triggering of
|
||||
* swipe navigation.
|
||||
*/
|
||||
isSwipePrevented( target ) {
|
||||
|
||||
// Prevent accidental swipes when scrubbing timelines
|
||||
if( matches( target, 'video, audio' ) ) return true;
|
||||
|
||||
while( target && typeof target.hasAttribute === 'function' ) {
|
||||
if( target.hasAttribute( 'data-prevent-swipe' ) ) return true;
|
||||
target = target.parentNode;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the 'touchstart' event, enables support for
|
||||
* swipe and pinch gestures.
|
||||
*
|
||||
* @param {object} event
|
||||
*/
|
||||
onTouchStart( event ) {
|
||||
|
||||
if( this.isSwipePrevented( event.target ) ) return true;
|
||||
|
||||
this.touchStartX = event.touches[0].clientX;
|
||||
this.touchStartY = event.touches[0].clientY;
|
||||
this.touchStartCount = event.touches.length;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the 'touchmove' event.
|
||||
*
|
||||
* @param {object} event
|
||||
*/
|
||||
onTouchMove( event ) {
|
||||
|
||||
if( this.isSwipePrevented( event.target ) ) return true;
|
||||
|
||||
let config = this.Reveal.getConfig();
|
||||
|
||||
// Each touch should only trigger one action
|
||||
if( !this.touchCaptured ) {
|
||||
this.Reveal.onUserInput( event );
|
||||
|
||||
let currentX = event.touches[0].clientX;
|
||||
let currentY = event.touches[0].clientY;
|
||||
|
||||
// There was only one touch point, look for a swipe
|
||||
if( event.touches.length === 1 && this.touchStartCount !== 2 ) {
|
||||
|
||||
let availableRoutes = this.Reveal.availableRoutes({ includeFragments: true });
|
||||
|
||||
let deltaX = currentX - this.touchStartX,
|
||||
deltaY = currentY - this.touchStartY;
|
||||
|
||||
if( deltaX > SWIPE_THRESHOLD && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
|
||||
this.touchCaptured = true;
|
||||
if( config.navigationMode === 'linear' ) {
|
||||
if( config.rtl ) {
|
||||
this.Reveal.next();
|
||||
}
|
||||
else {
|
||||
this.Reveal.prev();
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.Reveal.left();
|
||||
}
|
||||
}
|
||||
else if( deltaX < -SWIPE_THRESHOLD && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
|
||||
this.touchCaptured = true;
|
||||
if( config.navigationMode === 'linear' ) {
|
||||
if( config.rtl ) {
|
||||
this.Reveal.prev();
|
||||
}
|
||||
else {
|
||||
this.Reveal.next();
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.Reveal.right();
|
||||
}
|
||||
}
|
||||
else if( deltaY > SWIPE_THRESHOLD && availableRoutes.up ) {
|
||||
this.touchCaptured = true;
|
||||
if( config.navigationMode === 'linear' ) {
|
||||
this.Reveal.prev();
|
||||
}
|
||||
else {
|
||||
this.Reveal.up();
|
||||
}
|
||||
}
|
||||
else if( deltaY < -SWIPE_THRESHOLD && availableRoutes.down ) {
|
||||
this.touchCaptured = true;
|
||||
if( config.navigationMode === 'linear' ) {
|
||||
this.Reveal.next();
|
||||
}
|
||||
else {
|
||||
this.Reveal.down();
|
||||
}
|
||||
}
|
||||
|
||||
// If we're embedded, only block touch events if they have
|
||||
// triggered an action
|
||||
if( config.embedded ) {
|
||||
if( this.touchCaptured || this.Reveal.isVerticalSlide() ) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
// Not embedded? Block them all to avoid needless tossing
|
||||
// around of the viewport in iOS
|
||||
else {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
// There's a bug with swiping on some Android devices unless
|
||||
// the default action is always prevented
|
||||
else if( isAndroid ) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the 'touchend' event.
|
||||
*
|
||||
* @param {object} event
|
||||
*/
|
||||
onTouchEnd( event ) {
|
||||
|
||||
this.touchCaptured = false;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert pointer down to touch start.
|
||||
*
|
||||
* @param {object} event
|
||||
*/
|
||||
onPointerDown( event ) {
|
||||
|
||||
if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
|
||||
event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
|
||||
this.onTouchStart( event );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert pointer move to touch move.
|
||||
*
|
||||
* @param {object} event
|
||||
*/
|
||||
onPointerMove( event ) {
|
||||
|
||||
if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
|
||||
event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
|
||||
this.onTouchMove( event );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert pointer up to touch end.
|
||||
*
|
||||
* @param {object} event
|
||||
*/
|
||||
onPointerUp( event ) {
|
||||
|
||||
if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
|
||||
event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
|
||||
this.onTouchEnd( event );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
58
js/index.js
Normal file
58
js/index.js
Normal file
@ -0,0 +1,58 @@
|
||||
import Deck, { VERSION } from './reveal.js'
|
||||
|
||||
/**
|
||||
* Expose the Reveal class to the window. To create a
|
||||
* new instance:
|
||||
* let deck = new Reveal( document.querySelector( '.reveal' ), {
|
||||
* controls: false
|
||||
* } );
|
||||
* deck.initialize().then(() => {
|
||||
* // reveal.js is ready
|
||||
* });
|
||||
*/
|
||||
let Reveal = Deck;
|
||||
|
||||
|
||||
/**
|
||||
* The below is a thin shell that mimics the pre 4.0
|
||||
* reveal.js API and ensures backwards compatibility.
|
||||
* This API only allows for one Reveal instance per
|
||||
* page, whereas the new API above lets you run many
|
||||
* presentations on the same page.
|
||||
*
|
||||
* Reveal.initialize( { controls: false } ).then(() => {
|
||||
* // reveal.js is ready
|
||||
* });
|
||||
*/
|
||||
|
||||
let enqueuedAPICalls = [];
|
||||
|
||||
Reveal.initialize = options => {
|
||||
|
||||
// Create our singleton reveal.js instance
|
||||
Object.assign( Reveal, new Deck( document.querySelector( '.reveal' ), options ) );
|
||||
|
||||
// Invoke any enqueued API calls
|
||||
enqueuedAPICalls.map( method => method( Reveal ) );
|
||||
|
||||
return Reveal.initialize();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The pre 4.0 API let you add event listener before
|
||||
* initializing. We maintain the same behavior by
|
||||
* queuing up premature API calls and invoking all
|
||||
* of them when Reveal.initialize is called.
|
||||
*/
|
||||
[ 'configure', 'on', 'off', 'addEventListener', 'removeEventListener', 'registerPlugin' ].forEach( method => {
|
||||
Reveal[method] = ( ...args ) => {
|
||||
enqueuedAPICalls.push( deck => deck[method].call( null, ...args ) );
|
||||
}
|
||||
} );
|
||||
|
||||
Reveal.isReady = () => false;
|
||||
|
||||
Reveal.VERSION = VERSION;
|
||||
|
||||
export default Reveal;
|
2840
js/reveal.js
Normal file
2840
js/reveal.js
Normal file
File diff suppressed because it is too large
Load Diff
77
js/utils/color.js
Normal file
77
js/utils/color.js
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Converts various color input formats to an {r:0,g:0,b:0} object.
|
||||
*
|
||||
* @param {string} color The string representation of a color
|
||||
* @example
|
||||
* colorToRgb('#000');
|
||||
* @example
|
||||
* colorToRgb('#000000');
|
||||
* @example
|
||||
* colorToRgb('rgb(0,0,0)');
|
||||
* @example
|
||||
* colorToRgb('rgba(0,0,0)');
|
||||
*
|
||||
* @return {{r: number, g: number, b: number, [a]: number}|null}
|
||||
*/
|
||||
export const colorToRgb = ( color ) => {
|
||||
|
||||
let hex3 = color.match( /^#([0-9a-f]{3})$/i );
|
||||
if( hex3 && hex3[1] ) {
|
||||
hex3 = hex3[1];
|
||||
return {
|
||||
r: parseInt( hex3.charAt( 0 ), 16 ) * 0x11,
|
||||
g: parseInt( hex3.charAt( 1 ), 16 ) * 0x11,
|
||||
b: parseInt( hex3.charAt( 2 ), 16 ) * 0x11
|
||||
};
|
||||
}
|
||||
|
||||
let hex6 = color.match( /^#([0-9a-f]{6})$/i );
|
||||
if( hex6 && hex6[1] ) {
|
||||
hex6 = hex6[1];
|
||||
return {
|
||||
r: parseInt( hex6.slice( 0, 2 ), 16 ),
|
||||
g: parseInt( hex6.slice( 2, 4 ), 16 ),
|
||||
b: parseInt( hex6.slice( 4, 6 ), 16 )
|
||||
};
|
||||
}
|
||||
|
||||
let rgb = color.match( /^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i );
|
||||
if( rgb ) {
|
||||
return {
|
||||
r: parseInt( rgb[1], 10 ),
|
||||
g: parseInt( rgb[2], 10 ),
|
||||
b: parseInt( rgb[3], 10 )
|
||||
};
|
||||
}
|
||||
|
||||
let rgba = color.match( /^rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\,\s*([\d]+|[\d]*.[\d]+)\s*\)$/i );
|
||||
if( rgba ) {
|
||||
return {
|
||||
r: parseInt( rgba[1], 10 ),
|
||||
g: parseInt( rgba[2], 10 ),
|
||||
b: parseInt( rgba[3], 10 ),
|
||||
a: parseFloat( rgba[4] )
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates brightness on a scale of 0-255.
|
||||
*
|
||||
* @param {string} color See colorToRgb for supported formats.
|
||||
* @see {@link colorToRgb}
|
||||
*/
|
||||
export const colorBrightness = ( color ) => {
|
||||
|
||||
if( typeof color === 'string' ) color = colorToRgb( color );
|
||||
|
||||
if( color ) {
|
||||
return ( color.r * 299 + color.g * 587 + color.b * 114 ) / 1000;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
10
js/utils/constants.js
Normal file
10
js/utils/constants.js
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
export const SLIDES_SELECTOR = '.slides section';
|
||||
export const HORIZONTAL_SLIDES_SELECTOR = '.slides>section';
|
||||
export const VERTICAL_SLIDES_SELECTOR = '.slides>section.present>section';
|
||||
|
||||
// Methods that may not be invoked via the postMessage API
|
||||
export const POST_MESSAGE_METHOD_BLACKLIST = /registerPlugin|registerKeyboardShortcut|addKeyBinding|addEventListener|showPreview/;
|
||||
|
||||
// Regex for retrieving the fragment style from a class attribute
|
||||
export const FRAGMENT_STYLE_REGEX = /fade-(down|up|right|left|out|in-then-out|in-then-semi-out)|semi-fade-out|current-visible|shrink|grow/;
|
8
js/utils/device.js
Normal file
8
js/utils/device.js
Normal file
@ -0,0 +1,8 @@
|
||||
const UA = navigator.userAgent;
|
||||
|
||||
export const isMobile = /(iphone|ipod|ipad|android)/gi.test( UA ) ||
|
||||
( navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1 ); // iPadOS
|
||||
|
||||
export const isChrome = /chrome/i.test( UA ) && !/edge/i.test( UA );
|
||||
|
||||
export const isAndroid = /android/gi.test( UA );
|
46
js/utils/loader.js
Normal file
46
js/utils/loader.js
Normal file
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Loads a JavaScript file from the given URL and executes it.
|
||||
*
|
||||
* @param {string} url Address of the .js file to load
|
||||
* @param {function} callback Method to invoke when the script
|
||||
* has loaded and executed
|
||||
*/
|
||||
export const loadScript = ( url, callback ) => {
|
||||
|
||||
const script = document.createElement( 'script' );
|
||||
script.type = 'text/javascript';
|
||||
script.async = false;
|
||||
script.defer = false;
|
||||
script.src = url;
|
||||
|
||||
if( typeof callback === 'function' ) {
|
||||
|
||||
// Success callback
|
||||
script.onload = script.onreadystatechange = event => {
|
||||
if( event.type === 'load' || /loaded|complete/.test( script.readyState ) ) {
|
||||
|
||||
// Kill event listeners
|
||||
script.onload = script.onreadystatechange = script.onerror = null;
|
||||
|
||||
callback();
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
// Error callback
|
||||
script.onerror = err => {
|
||||
|
||||
// Kill event listeners
|
||||
script.onload = script.onreadystatechange = script.onerror = null;
|
||||
|
||||
callback( new Error( 'Failed loading script: ' + script.src + '\n' + err ) );
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// Append the script at the end of <head>
|
||||
const head = document.querySelector( 'head' );
|
||||
head.insertBefore( script, head.lastChild );
|
||||
|
||||
}
|
313
js/utils/util.js
Normal file
313
js/utils/util.js
Normal file
@ -0,0 +1,313 @@
|
||||
/**
|
||||
* Extend object a with the properties of object b.
|
||||
* If there's a conflict, object b takes precedence.
|
||||
*
|
||||
* @param {object} a
|
||||
* @param {object} b
|
||||
*/
|
||||
export const extend = ( a, b ) => {
|
||||
|
||||
for( let i in b ) {
|
||||
a[ i ] = b[ i ];
|
||||
}
|
||||
|
||||
return a;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* querySelectorAll but returns an Array.
|
||||
*/
|
||||
export const queryAll = ( el, selector ) => {
|
||||
|
||||
return Array.from( el.querySelectorAll( selector ) );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* classList.toggle() with cross browser support
|
||||
*/
|
||||
export const toggleClass = ( el, className, value ) => {
|
||||
if( value ) {
|
||||
el.classList.add( className );
|
||||
}
|
||||
else {
|
||||
el.classList.remove( className );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility for deserializing a value.
|
||||
*
|
||||
* @param {*} value
|
||||
* @return {*}
|
||||
*/
|
||||
export const deserialize = ( value ) => {
|
||||
|
||||
if( typeof value === 'string' ) {
|
||||
if( value === 'null' ) return null;
|
||||
else if( value === 'true' ) return true;
|
||||
else if( value === 'false' ) return false;
|
||||
else if( value.match( /^-?[\d\.]+$/ ) ) return parseFloat( value );
|
||||
}
|
||||
|
||||
return value;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Measures the distance in pixels between point a
|
||||
* and point b.
|
||||
*
|
||||
* @param {object} a point with x/y properties
|
||||
* @param {object} b point with x/y properties
|
||||
*
|
||||
* @return {number}
|
||||
*/
|
||||
export const distanceBetween = ( a, b ) => {
|
||||
|
||||
let dx = a.x - b.x,
|
||||
dy = a.y - b.y;
|
||||
|
||||
return Math.sqrt( dx*dx + dy*dy );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a CSS transform to the target element.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* @param {string} transform
|
||||
*/
|
||||
export const transformElement = ( element, transform ) => {
|
||||
|
||||
element.style.transform = transform;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Element.matches with IE support.
|
||||
*
|
||||
* @param {HTMLElement} target The element to match
|
||||
* @param {String} selector The CSS selector to match
|
||||
* the element against
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
export const matches = ( target, selector ) => {
|
||||
|
||||
let matchesMethod = target.matches || target.matchesSelector || target.msMatchesSelector;
|
||||
|
||||
return !!( matchesMethod && matchesMethod.call( target, selector ) );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the closest parent that matches the given
|
||||
* selector.
|
||||
*
|
||||
* @param {HTMLElement} target The child element
|
||||
* @param {String} selector The CSS selector to match
|
||||
* the parents against
|
||||
*
|
||||
* @return {HTMLElement} The matched parent or null
|
||||
* if no matching parent was found
|
||||
*/
|
||||
export const closest = ( target, selector ) => {
|
||||
|
||||
// Native Element.closest
|
||||
if( typeof target.closest === 'function' ) {
|
||||
return target.closest( selector );
|
||||
}
|
||||
|
||||
// Polyfill
|
||||
while( target ) {
|
||||
if( matches( target, selector ) ) {
|
||||
return target;
|
||||
}
|
||||
|
||||
// Keep searching
|
||||
target = target.parentNode;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handling the fullscreen functionality via the fullscreen API
|
||||
*
|
||||
* @see http://fullscreen.spec.whatwg.org/
|
||||
* @see https://developer.mozilla.org/en-US/docs/DOM/Using_fullscreen_mode
|
||||
*/
|
||||
export const enterFullscreen = element => {
|
||||
|
||||
element = element || document.documentElement;
|
||||
|
||||
// Check which implementation is available
|
||||
let requestMethod = element.requestFullscreen ||
|
||||
element.webkitRequestFullscreen ||
|
||||
element.webkitRequestFullScreen ||
|
||||
element.mozRequestFullScreen ||
|
||||
element.msRequestFullscreen;
|
||||
|
||||
if( requestMethod ) {
|
||||
requestMethod.apply( element );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an HTML element and returns a reference to it.
|
||||
* If the element already exists the existing instance will
|
||||
* be returned.
|
||||
*
|
||||
* @param {HTMLElement} container
|
||||
* @param {string} tagname
|
||||
* @param {string} classname
|
||||
* @param {string} innerHTML
|
||||
*
|
||||
* @return {HTMLElement}
|
||||
*/
|
||||
export const createSingletonNode = ( container, tagname, classname, innerHTML='' ) => {
|
||||
|
||||
// Find all nodes matching the description
|
||||
let nodes = container.querySelectorAll( '.' + classname );
|
||||
|
||||
// Check all matches to find one which is a direct child of
|
||||
// the specified container
|
||||
for( let i = 0; i < nodes.length; i++ ) {
|
||||
let testNode = nodes[i];
|
||||
if( testNode.parentNode === container ) {
|
||||
return testNode;
|
||||
}
|
||||
}
|
||||
|
||||
// If no node was found, create it now
|
||||
let node = document.createElement( tagname );
|
||||
node.className = classname;
|
||||
node.innerHTML = innerHTML;
|
||||
container.appendChild( node );
|
||||
|
||||
return node;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the given CSS styles into the DOM.
|
||||
*
|
||||
* @param {string} value
|
||||
*/
|
||||
export const createStyleSheet = ( value ) => {
|
||||
|
||||
let tag = document.createElement( 'style' );
|
||||
tag.type = 'text/css';
|
||||
|
||||
if( value && value.length > 0 ) {
|
||||
if( tag.styleSheet ) {
|
||||
tag.styleSheet.cssText = value;
|
||||
}
|
||||
else {
|
||||
tag.appendChild( document.createTextNode( value ) );
|
||||
}
|
||||
}
|
||||
|
||||
document.head.appendChild( tag );
|
||||
|
||||
return tag;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a key:value hash of all query params.
|
||||
*/
|
||||
export const getQueryHash = () => {
|
||||
|
||||
let query = {};
|
||||
|
||||
location.search.replace( /[A-Z0-9]+?=([\w\.%-]*)/gi, a => {
|
||||
query[ a.split( '=' ).shift() ] = a.split( '=' ).pop();
|
||||
} );
|
||||
|
||||
// Basic deserialization
|
||||
for( let i in query ) {
|
||||
let value = query[ i ];
|
||||
|
||||
query[ i ] = deserialize( unescape( value ) );
|
||||
}
|
||||
|
||||
// Do not accept new dependencies via query config to avoid
|
||||
// the potential of malicious script injection
|
||||
if( typeof query['dependencies'] !== 'undefined' ) delete query['dependencies'];
|
||||
|
||||
return query;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the remaining height within the parent of the
|
||||
* target element.
|
||||
*
|
||||
* remaining height = [ configured parent height ] - [ current parent height ]
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* @param {number} [height]
|
||||
*/
|
||||
export const getRemainingHeight = ( element, height = 0 ) => {
|
||||
|
||||
if( element ) {
|
||||
let newHeight, oldHeight = element.style.height;
|
||||
|
||||
// Change the .stretch element height to 0 in order find the height of all
|
||||
// the other elements
|
||||
element.style.height = '0px';
|
||||
|
||||
// In Overview mode, the parent (.slide) height is set of 700px.
|
||||
// Restore it temporarily to its natural height.
|
||||
element.parentNode.style.height = 'auto';
|
||||
|
||||
newHeight = height - element.parentNode.offsetHeight;
|
||||
|
||||
// Restore the old height, just in case
|
||||
element.style.height = oldHeight + 'px';
|
||||
|
||||
// Clear the parent (.slide) height. .removeProperty works in IE9+
|
||||
element.parentNode.style.removeProperty('height');
|
||||
|
||||
return newHeight;
|
||||
}
|
||||
|
||||
return height;
|
||||
|
||||
}
|
||||
|
||||
const fileExtensionToMimeMap = {
|
||||
'mp4': 'video/mp4',
|
||||
'm4a': 'video/mp4',
|
||||
'ogv': 'video/ogg',
|
||||
'mpeg': 'video/mpeg',
|
||||
'webm': 'video/webm'
|
||||
}
|
||||
|
||||
/**
|
||||
* Guess the MIME type for common file formats.
|
||||
*/
|
||||
export const getMimeTypeFromFile = ( filename='' ) => {
|
||||
return fileExtensionToMimeMap[filename.split('.').pop()]
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a string for RFC3986-compliant URL format.
|
||||
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI#encoding_for_rfc3986
|
||||
*
|
||||
* @param {string} url
|
||||
*/
|
||||
export const encodeRFC3986URI = ( url='' ) => {
|
||||
return encodeURI(url)
|
||||
.replace(/%5B/g, "[")
|
||||
.replace(/%5D/g, "]")
|
||||
.replace(
|
||||
/[!'()*]/g,
|
||||
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user