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,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();
}
}