@wordpress/block-library
Version:
Block library for the WordPress editor.
273 lines (254 loc) • 8.31 kB
JavaScript
import { useEffect, useRef } from '@wordpress/element';
import { useEvent, useRefEffect } from '@wordpress/compose';
import { __, _x } from '@wordpress/i18n';
import {
applyWaveformPlayerStyles,
initWaveformPlayer,
setupPlayButtonArtwork,
updateSeekControlLabel,
} from './waveform-utils';
/**
* Update the metadata of a WaveformPlayer element to reflect current props.
*
* `loadTrack()` owns full track swaps, but it also resets the audio element.
* This keeps same-source metadata edits lightweight in the editor.
*
* @param {Object} player - The waveform player.
* @param {Object} metadata - The track metadata.
* @param {string} metadata.title - The track title.
* @param {string} metadata.artist - The artist name.
* @param {string} metadata.image - The track image URL.
* @param {string} metadata.imageAlt - The track image alt text.
* @param {boolean} showPlayButtonArtwork - Whether to show artwork on the play button.
*/
function updatePlayerMetadata(
player,
{ title, artist, image, imageAlt },
showPlayButtonArtwork
) {
const { instance, container } = player;
const playerArtwork = showPlayButtonArtwork ? undefined : image;
if ( instance.titleEl ) {
instance.titleEl.textContent = title ?? '';
}
updateSeekControlLabel( instance, title || __( 'Seek' ) );
if ( typeof instance.syncArtist === 'function' ) {
instance.syncArtist( artist || '' );
} else if ( instance.artistEl ) {
instance.artistEl.textContent = artist ?? '';
instance.artistEl.style.display = artist ? '' : 'none';
}
if ( typeof instance.syncArtwork === 'function' ) {
instance.syncArtwork(
playerArtwork || null,
playerArtwork ? imageAlt || '' : ''
);
} else if ( instance.artworkEl && playerArtwork ) {
instance.artworkEl.src = playerArtwork;
instance.artworkEl.alt = imageAlt || '';
}
if ( showPlayButtonArtwork ) {
setupPlayButtonArtwork( container, image );
}
}
/**
* A reusable WaveformPlayer component for the block editor.
*
* Renders an audio waveform visualization with play/pause controls.
* Automatically inherits colors from the parent block's text color.
*
* @param {Object} props - Component props.
* @param {string} props.src - The audio file URL.
* @param {string} props.title - The track title.
* @param {string} props.artist - The artist name.
* @param {string} props.image - The track image URL.
* @param {string} props.imageAlt - The track image alt text.
* @param {string} props.color - The waveform color.
* @param {string} props.gradient - The waveform gradient.
* @param {string} props.backgroundColor - The waveform background color.
* @param {string} props.backgroundGradient - The waveform background gradient.
* @param {string} props.textColor - The player text color.
* @param {string} props.waveformStyle - Waveform style (bars, mirror, line, blocks, dots, seekbar).
* @param {Function} props.onEnded - Callback when the track finishes playing.
* @param {boolean} props.showPlayButtonArtwork - Whether to show artwork on the play button.
* @return {Element} The WaveformPlayer element.
*/
export function WaveformPlayer( {
src,
title,
artist,
image,
imageAlt,
color,
gradient,
backgroundColor,
backgroundGradient,
textColor,
waveformStyle,
onEnded,
showPlayButtonArtwork = false,
} ) {
// Store onEnded in a stable callback so it doesn't need to be a useRefEffect dependency.
// The callback changes reference on every render (its dependency chain
// includes an unstable array), which would cause useRefEffect to destroy
// and recreate the entire player on every re-render, making it disappear
// during editor resizes.
const onEndedEvent = useEvent( onEnded );
// Ref for the WaveformPlayer instance
const playerRef = useRef();
// WaveformPlayer needs an audio source on init, but the source may change
// throughout its lifetime.
const hasSrc = !! src;
// Combined props ref for `initWaveformPlayer`, which is called
// asynchronously after this component mounts.
const metadataRef = useRef( { src, title, artist, image, imageAlt } );
const stylesRef = useRef( {
color,
gradient,
backgroundColor,
backgroundGradient,
textColor,
} );
useEffect( () => {
metadataRef.current = { src, title, artist, image, imageAlt };
}, [ src, title, artist, image, imageAlt ] );
useEffect( () => {
stylesRef.current = {
color,
gradient,
backgroundColor,
backgroundGradient,
textColor,
};
}, [ color, gradient, backgroundColor, backgroundGradient, textColor ] );
useEffect( () => {
if ( playerRef.current?.container ) {
applyWaveformPlayerStyles( playerRef.current.container, {
backgroundColor,
backgroundGradient,
textColor,
playButtonColor: showPlayButtonArtwork ? undefined : color,
playButtonGradient: showPlayButtonArtwork
? undefined
: gradient,
} );
}
}, [
backgroundColor,
backgroundGradient,
color,
gradient,
showPlayButtonArtwork,
textColor,
] );
const ref = useRefEffect(
( element ) => {
if ( ! hasSrc ) {
return;
}
let cancelled = false;
let playerDestroy;
function init() {
if ( cancelled ) {
return;
}
const player = initWaveformPlayer( element, {
src: metadataRef.current.src,
title: metadataRef.current.title,
artist: metadataRef.current.artist,
image: metadataRef.current.image,
imageAlt: metadataRef.current.imageAlt,
waveformColor: stylesRef.current.color,
waveformGradient: stylesRef.current.gradient,
backgroundColor: stylesRef.current.backgroundColor,
backgroundGradient: stylesRef.current.backgroundGradient,
textColor: stylesRef.current.textColor,
waveformStyle,
labels: {
seek: __( 'Seek' ),
/* translators: %1$s: current audio time, %2$s: total audio duration. */
seekValueText: _x(
'%1$s of %2$s',
'audio current time of total duration'
),
},
onEnded: () => onEndedEvent?.(),
showPlayButtonArtwork,
} );
playerRef.current = player;
const { destroy } = player;
playerDestroy = destroy;
}
// Defer initialization so the element inherits the correct
// text color, which is used to derive waveform colors. In the
// editor iframe, theme styles (CSS custom properties) are
// injected dynamically, so getComputedStyle may return the
// default black on first render.
// Using a requestAnimationFrame loop isn't sufficient to solve the issue.
// TODO - find a better option than a setTimeout, so we're not relying on an arbitrary number.
const timeoutId = setTimeout( init, 100 );
return () => {
cancelled = true;
clearTimeout( timeoutId );
playerRef.current = undefined;
playerDestroy?.();
};
},
[
onEndedEvent,
hasSrc,
waveformStyle,
color,
gradient,
textColor,
showPlayButtonArtwork,
]
);
useEffect( () => {
if ( playerRef.current?.instance ) {
const player = playerRef.current;
if ( player ) {
updatePlayerMetadata(
player,
{
title,
artist,
image,
imageAlt,
},
showPlayButtonArtwork
);
}
}
}, [ title, artist, image, imageAlt, showPlayButtonArtwork ] );
useEffect( () => {
if ( src && playerRef.current?.instance ) {
const wasPlaying = playerRef.current.instance.isPlaying;
const promise = playerRef.current.instance.loadTrack(
src,
metadataRef.current.title,
metadataRef.current.artist,
{
artwork: showPlayButtonArtwork
? undefined
: metadataRef.current.image,
artworkAlt: showPlayButtonArtwork
? ''
: metadataRef.current.imageAlt,
}
);
promise.then( () => {
if ( showPlayButtonArtwork && playerRef.current?.container ) {
setupPlayButtonArtwork(
playerRef.current.container,
metadataRef.current.image
);
}
if ( ! wasPlaying ) {
playerRef.current?.instance.pause();
}
} );
}
}, [ src, showPlayButtonArtwork ] );
return <div ref={ ref } className="wp-block-playlist__waveform-player" />;
}