update packages and add valign

This commit is contained in:
2026-04-05 20:00:27 +02:00
parent b062fb98e3
commit 03fb00e374
640 changed files with 109768 additions and 39311 deletions

View File

@@ -1,5 +1,4 @@
import { queryAll, extend, createStyleSheet, matches, closest } from '../utils/util.js'
import { FRAGMENT_STYLE_REGEX } from '../utils/constants.js'
import { queryAll, extend, createStyleSheet, matches, closest } from '../utils/util'
// Counter used to generate unique IDs for auto-animated elements
let autoAnimateCounter = 0;

View File

@@ -1,5 +1,5 @@
import { queryAll } from '../utils/util.js'
import { colorToRgb, colorBrightness } from '../utils/color.js'
import { queryAll } from '../utils/util'
import { colorToRgb, colorBrightness } from '../utils/color'
/**
* Creates and updates slide backgrounds.

View File

@@ -1,5 +1,5 @@
import { queryAll, enterFullscreen } from '../utils/util.js'
import { isAndroid } from '../utils/device.js'
import { queryAll, enterFullscreen } from '../utils/util'
import { isAndroid } from '../utils/device'
/**
* Manages our presentation controls. This includes both
@@ -66,9 +66,11 @@ export default class Controls {
*/
configure( config, oldConfig ) {
const speakerOnly = config.controls === 'speaker' || config.controls === 'speaker-only';
this.element.style.display = (
config.controls &&
(config.controls !== 'speaker-only' || this.Reveal.isSpeakerNotes())
(!speakerOnly || this.Reveal.isSpeakerNotes())
) ? 'block' : 'none';
this.element.setAttribute( 'data-controls-layout', config.controlsLayout );
@@ -83,9 +85,10 @@ export default class Controls {
let pointerEvents = [ 'touchstart', 'click' ];
// Only support touch for Android, fixes double navigations in
// stock browser
// stock browser. Use touchend for it to be considered a valid
// user interaction (so we're allowed to autoplay media).
if( isAndroid ) {
pointerEvents = [ 'touchstart' ];
pointerEvents = [ 'touchend' ];
}
pointerEvents.forEach( eventName => {
@@ -102,7 +105,7 @@ export default class Controls {
unbind() {
[ 'touchstart', 'click' ].forEach( eventName => {
[ 'touchstart', 'touchend', '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 ) );

View File

@@ -1,4 +1,4 @@
import { closest } from '../utils/util.js'
import { closest } from '../utils/util'
/**
* Manages focus when a presentation is embedded. This

View File

@@ -1,4 +1,4 @@
import { extend, queryAll } from '../utils/util.js'
import { extend, queryAll } from '../utils/util'
/**
* Handles sorting and navigation of slide fragments.

View File

@@ -1,4 +1,4 @@
import { enterFullscreen } from '../utils/util.js'
import { enterFullscreen } from '../utils/util'
/**
* Handles all reveal.js keyboard interactions.
@@ -398,6 +398,12 @@ export default class Keyboard {
event.preventDefault && event.preventDefault();
}
// Enter to exit overview mode
else if (keyCode === 13 && this.Reveal.overview.isActive()) {
this.Reveal.overview.deactivate();
event.preventDefault && event.preventDefault();
}
// If auto-sliding is enabled we need to cue up
// another timeout

View File

@@ -60,11 +60,13 @@ export default class Location {
name = name.split( '/' ).shift();
}
// Ensure the named link is a valid HTML ID attribute
// Ensure the named link is a valid HTML id or data-id attribute
try {
slide = document
.getElementById( decodeURIComponent( name ) )
.closest('.slides section');
const decodedName = decodeURIComponent( name );
slide = (
document.getElementById( decodedName ) ||
document.querySelector( `[data-id="${decodedName}"]` )
).closest('.slides section');
}
catch ( error ) { }

View File

@@ -72,8 +72,8 @@ export default class Overlay {
this.viewport.innerHTML =
`<header class="r-overlay-header">
<a class="r-overlay-button r-overlay-external" href="${url}" target="_blank"><span class="icon"></span></a>
<button class="r-overlay-button r-overlay-close"><span class="icon"></span></button>
<a class="r-overlay-header-button r-overlay-external" href="${url}" target="_blank"><span class="icon"></span></a>
<button class="r-overlay-header-button r-overlay-close"><span class="icon"></span></button>
</header>
<div class="r-overlay-spinner"></div>
<div class="r-overlay-content">
@@ -125,7 +125,7 @@ export default class Overlay {
this.viewport.innerHTML =
`<header class="r-overlay-header">
<button class="r-overlay-button r-overlay-close">Esc <span class="icon"></span></button>
<button class="r-overlay-header-button r-overlay-close">Esc <span class="icon"></span></button>
</header>
<div class="r-overlay-spinner"></div>
<div class="r-overlay-content"></div>`;
@@ -262,7 +262,7 @@ export default class Overlay {
this.viewport.innerHTML = `
<header class="r-overlay-header">
<button class="r-overlay-button r-overlay-close">Esc <span class="icon"></span></button>
<button class="r-overlay-header-button r-overlay-close">Esc <span class="icon"></span></button>
</header>
<div class="r-overlay-content">
<div class="r-overlay-help-content">${html}</div>
@@ -348,7 +348,9 @@ export default class Overlay {
// Let the browser handle meta keys naturally so users can cmd+click
return;
}
let url = linkTarget.getAttribute( 'href' ) || linkTarget.getAttribute( 'data-preview-link' );
const dataPreviewLink = linkTarget.getAttribute( 'data-preview-link' );
const dataPreviewLinkIsUrl = typeof dataPreviewLink === 'string' && dataPreviewLink.startsWith( 'http' );
let url = dataPreviewLinkIsUrl ? dataPreviewLink : linkTarget.getAttribute( 'href' );
if( url ) {
this.previewIframe( url );
event.preventDefault();

View File

@@ -1,5 +1,5 @@
import { SLIDES_SELECTOR } from '../utils/constants.js'
import { extend, queryAll, transformElement } from '../utils/util.js'
import { SLIDES_SELECTOR } from '../utils/constants'
import { extend, queryAll, transformElement } from '../utils/util'
/**
* Handles all logic related to the overview mode

View File

@@ -1,4 +1,4 @@
import { loadScript } from '../utils/loader.js'
import { loadScript } from '../utils/loader'
/**
* Manages loading and registering of reveal.js plugins.
@@ -206,7 +206,6 @@ export default class Plugins {
else {
console.warn( 'reveal.js: "'+ id +'" plugin has already been registered' );
}
}
/**

View File

@@ -1,5 +1,5 @@
import { SLIDES_SELECTOR } from '../utils/constants.js'
import { queryAll, createStyleSheet } from '../utils/util.js'
import { SLIDES_SELECTOR } from '../utils/constants'
import { queryAll, createStyleSheet } from '../utils/util'
/**
* Setups up our presentation for printing/exporting to PDF.

View File

@@ -1,5 +1,5 @@
import { HORIZONTAL_SLIDES_SELECTOR, HORIZONTAL_BACKGROUNDS_SELECTOR } from '../utils/constants.js'
import { queryAll } from '../utils/util.js'
import { HORIZONTAL_SLIDES_SELECTOR, HORIZONTAL_BACKGROUNDS_SELECTOR } from '../utils/constants'
import { queryAll } from '../utils/util'
const HIDE_SCROLLBAR_TIMEOUT = 500;
const MAX_PROGRESS_SPACING = 4;

View File

@@ -1,5 +1,5 @@
import { extend, queryAll, closest, getMimeTypeFromFile, encodeRFC3986URI } from '../utils/util.js'
import { isMobile } from '../utils/device.js'
import { extend, queryAll, closest, getMimeTypeFromFile, encodeRFC3986URI } from '../utils/util'
import { isMobile } from '../utils/device'
import fitty from 'fitty';
@@ -9,11 +9,44 @@ import fitty from 'fitty';
*/
export default class SlideContent {
allowedToPlayAudio = null;
constructor( Reveal ) {
this.Reveal = Reveal;
this.startEmbeddedMedia = this.startEmbeddedMedia.bind( this );
this.startEmbeddedIframe = this.startEmbeddedIframe.bind( this );
this.preventIframeAutoFocus = this.preventIframeAutoFocus.bind( this );
this.ensureMobileMediaPlaying = this.ensureMobileMediaPlaying.bind( this );
this.failedAudioPlaybackTargets = new Set();
this.failedVideoPlaybackTargets = new Set();
this.failedMutedVideoPlaybackTargets = new Set();
this.renderMediaPlayButton();
}
renderMediaPlayButton() {
this.mediaPlayButton = document.createElement( 'button' );
this.mediaPlayButton.className = 'r-overlay-button r-media-play-button';
this.mediaPlayButton.addEventListener( 'click', () => {
this.resetTemporarilyMutedMedia();
const failedTargets = new Set( [
...this.failedAudioPlaybackTargets,
...this.failedVideoPlaybackTargets,
...this.failedMutedVideoPlaybackTargets
] );
failedTargets.forEach( target => {
this.startEmbeddedMedia( { target: target } );
} );
this.clearMediaPlaybackErrors();
} );
}
@@ -51,14 +84,25 @@ export default class SlideContent {
load( slide, options = {} ) {
// Show the slide element
slide.style.display = this.Reveal.getConfig().display;
const displayValue = this.Reveal.getConfig().display;
if( displayValue.includes('!important') ) {
const value = displayValue.replace(/\s*!important\s*$/, '').trim();
slide.style.setProperty('display', value, 'important');
} else {
slide.style.display = displayValue;
}
// Media elements with data-src attributes
// Media and iframe 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 ) ) {
const isIframe = element.tagName === 'IFRAME';
if( !isIframe || this.shouldPreload( element ) ) {
element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
element.setAttribute( 'data-lazy-loaded', '' );
element.removeAttribute( 'data-src' );
if( isIframe ) {
element.addEventListener( 'load', this.preventIframeAutoFocus );
}
}
} );
@@ -131,12 +175,7 @@ export default class SlideContent {
}
// 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', '' );
}
@@ -318,20 +357,9 @@ export default class SlideContent {
// Mobile devices never fire a loaded event so instead
// of waiting, we initiate playback
else if( isMobile ) {
let promise = el.play();
el.addEventListener( 'canplay', this.ensureMobileMediaPlaying );
// 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;
} );
} );
}
this.playMediaElement( el );
}
// If the media isn't loaded, wait before playing
else {
@@ -374,6 +402,40 @@ export default class SlideContent {
}
/**
* Ensure that an HTMLMediaElement is playing on mobile devices.
*
* This is a workaround for a bug in mobile Safari where
* the media fails to display if many videos are started
* at the same moment. When this happens, Mobile Safari
* reports the video is playing, and the current time
* advances, but nothing is visible.
*
* @param {Event} event
*/
ensureMobileMediaPlaying( event ) {
const el = event.target;
// Ignore this check incompatible browsers
if( typeof el.getVideoPlaybackQuality !== 'function' ) {
return;
}
setTimeout( () => {
const playing = el.paused === false;
const totalFrames = el.getVideoPlaybackQuality().totalVideoFrames;
if( playing && totalFrames === 0 ) {
el.load();
el.play();
}
}, 1000 );
}
/**
* Starts playing an embedded video/audio element after
* it has finished loading.
@@ -389,7 +451,7 @@ export default class SlideContent {
// Don't restart if media is already playing
if( event.target.paused || event.target.ended ) {
event.target.currentTime = 0;
event.target.play();
this.playMediaElement( event.target );
}
}
@@ -397,6 +459,55 @@ export default class SlideContent {
}
/**
* Plays the given HTMLMediaElement and handles any playback
* errors, such as the browser not allowing audio to play without
* user action.
*
* @param {HTMLElement} mediaElement
*/
playMediaElement( mediaElement ) {
const promise = mediaElement.play();
if( promise && typeof promise.catch === 'function' ) {
promise
.then( () => {
if( !mediaElement.muted ) {
this.allowedToPlayAudio = true;
}
} )
.catch( ( error ) => {
if( error.name === 'NotAllowedError' ) {
this.allowedToPlayAudio = false;
// If this is a video, we record the error and try to play it
// muted as a fallback. The user will be presented with an unmute
// button.
if( mediaElement.tagName === 'VIDEO' ) {
this.onVideoPlaybackNotAllowed( mediaElement );
let isAttachedToDOM = !!closest( mediaElement, 'html' ),
isVisible = !!closest( mediaElement, '.present' ),
isMuted = mediaElement.muted;
if( isAttachedToDOM && isVisible && !isMuted ) {
mediaElement.setAttribute( 'data-muted-by-reveal', 'true' );
mediaElement.muted = true;
mediaElement.play().catch(() => {
this.onMutedVideoPlaybackNotAllowed( mediaElement );
});
}
}
else if( mediaElement.tagName === 'AUDIO' ) {
this.onAudioPlaybackNotAllowed( mediaElement );
}
}
} );
}
}
/**
* "Starts" the content of an embedded iframe using the
* postMessage API.
@@ -407,6 +518,8 @@ export default class SlideContent {
let iframe = event.target;
this.preventIframeAutoFocus( event );
if( iframe && iframe.contentWindow ) {
let isAttachedToDOM = !!closest( event.target, 'html' ),
@@ -461,12 +574,17 @@ export default class SlideContent {
if( !el.hasAttribute( 'data-ignore' ) && typeof el.pause === 'function' ) {
el.setAttribute('data-paused-by-reveal', '');
el.pause();
if( isMobile ) {
el.removeEventListener( 'canplay', this.ensureMobileMediaPlaying );
}
}
} );
// 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.preventIframeAutoFocus );
el.removeEventListener( 'load', this.startEmbeddedIframe );
});
@@ -497,4 +615,132 @@ export default class SlideContent {
}
/**
* Checks whether media playback is blocked by the browser. This
* typically happens when media playback is initiated without a
* direct user interaction.
*/
isAllowedToPlayAudio() {
return this.allowedToPlayAudio;
}
/**
* Shows a manual button in situations where autoamtic media playback
* is not allowed by the browser.
*/
showPlayOrUnmuteButton() {
const audioTargets = this.failedAudioPlaybackTargets.size;
const videoTargets = this.failedVideoPlaybackTargets.size;
const mutedVideoTargets = this.failedMutedVideoPlaybackTargets.size;
let label = 'Play media';
if( mutedVideoTargets > 0 ) {
label = 'Play video';
}
else if( videoTargets > 0 ) {
label = 'Unmute video';
}
else if( audioTargets > 0 ) {
label = 'Play audio';
}
this.mediaPlayButton.textContent = label;
this.Reveal.getRevealElement().appendChild( this.mediaPlayButton );
}
onAudioPlaybackNotAllowed( target ) {
this.failedAudioPlaybackTargets.add( target );
this.showPlayOrUnmuteButton( target );
}
onVideoPlaybackNotAllowed( target ) {
this.failedVideoPlaybackTargets.add( target );
this.showPlayOrUnmuteButton();
}
onMutedVideoPlaybackNotAllowed( target ) {
this.failedMutedVideoPlaybackTargets.add( target );
this.showPlayOrUnmuteButton();
}
/**
* Videos may be temporarily muted by us to get around browser
* restrictions on automatic playback. This method rolls back
* all such temporary audio changes.
*/
resetTemporarilyMutedMedia() {
const failedTargets = new Set( [
...this.failedAudioPlaybackTargets,
...this.failedVideoPlaybackTargets,
...this.failedMutedVideoPlaybackTargets
] );
failedTargets.forEach( target => {
if( target.hasAttribute( 'data-muted-by-reveal' ) ) {
target.muted = false;
target.removeAttribute( 'data-muted-by-reveal' );
}
} );
}
clearMediaPlaybackErrors() {
this.resetTemporarilyMutedMedia();
this.failedAudioPlaybackTargets.clear();
this.failedVideoPlaybackTargets.clear();
this.failedMutedVideoPlaybackTargets.clear();
this.mediaPlayButton.remove();
}
/**
* Prevents iframes from automatically focusing themselves.
*
* @param {Event} event
*/
preventIframeAutoFocus( event ) {
const iframe = event.target;
if( iframe && this.Reveal.getConfig().preventIframeAutoFocus ) {
let elapsed = 0;
const interval = 100;
const maxTime = 1000;
const checkFocus = () => {
if( document.activeElement === iframe ) {
document.activeElement.blur();
} else if( elapsed < maxTime ) {
elapsed += interval;
setTimeout( checkFocus, interval );
}
};
setTimeout( checkFocus, interval );
}
}
afterSlideChanged() {
this.clearMediaPlaybackErrors();
}
}

View File

@@ -1,5 +1,5 @@
import { isAndroid } from '../utils/device.js'
import { matches } from '../utils/util.js'
import { isAndroid } from '../utils/device'
import { matches } from '../utils/util'
const SWIPE_THRESHOLD = 40;
@@ -216,6 +216,14 @@ export default class Touch {
*/
onTouchEnd( event ) {
// Media playback is only allowed as a direct result of a
// user interaction. Some mobile devices do not consider a
// 'touchmove' to be a direct user action. If this is the
// case, we fall back to starting playback here instead.
if( this.touchCaptured && !this.Reveal.slideContent.isAllowedToPlayAudio() ) {
this.Reveal.startEmbeddedContent( this.Reveal.getCurrentSlide() );
}
this.touchCaptured = false;
}