@wordpress/block-library
Version:
Block library for the WordPress editor.
150 lines (136 loc) • 5.4 kB
JavaScript
/**
* WordPress dependencies
*/
import { useEffect, useRef } from '@wordpress/element';
import { useEvent, useRefEffect } from '@wordpress/compose';
/**
* Internal dependencies
*/
import { initWaveformPlayer } from './waveform-utils';
const EMPTY_ARTIST_PLACEHOLDER = '\u00a0';
/**
* Update a live waveform player's metadata elements in place.
*
* The title element always exists, so the title is updated in place. The
* subtitle element is seeded during editor player creation, so it can be
* updated in place and hidden when the track has no artist. The artwork
* element only exists when the track had an image when the player was created,
* so its value is updated in place here; adding or removing an image (which
* creates or tears down that element) is instead handled by recreating the
* player, keyed on the `hasImage` dependency.
*
* The library's only metadata API is `loadTrack()`, which re-fetches and
* re-decodes the audio and regenerates the waveform (resetting playback), so
* it's unsuitable for live metadata edits. We instead write to the title,
* subtitle, and artwork elements directly, which is what `loadTrack()` itself
* does internally for these fields.
*
* @param {Object} instance - The waveform player instance.
* @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 artwork image URL.
*/
function updatePlayerMetadata( instance, { title, artist, image } ) {
if ( instance.titleEl ) {
instance.titleEl.textContent = title ?? '';
}
if ( instance.subtitleEl ) {
instance.subtitleEl.textContent = artist ?? '';
instance.subtitleEl.style.display = artist ? '' : 'none';
}
if ( instance.artworkEl && image ) {
instance.artworkEl.src = 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 artwork image URL.
* @param {string} props.waveformStyle - Waveform style (bars, mirror, line, blocks, dots, seekbar).
* @param {Function} props.onEnded - Callback when the track finishes playing.
* @return {Element} The WaveformPlayer element.
*/
export function WaveformPlayer( {
src,
title,
artist,
image,
waveformStyle,
onEnded,
} ) {
// 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 );
const metadataRef = useRef( { title, artist, image } );
const playerRef = useRef();
// The artwork element only exists when an image was present when the
// player was created. Recreate the player when one is added or removed so
// that element is created or torn down; value changes to an existing
// element are applied in place below.
const hasImage = !! image;
// Keep the freshest metadata available to init() (which runs on a
// deferred timeout) and update the live player in place when metadata
// changes. Updating in place avoids destroying and recreating the
// player, which would flash it on every keystroke while editing a
// track's title or artist.
useEffect( () => {
metadataRef.current = { title, artist, image };
const instance = playerRef.current?.instance;
if ( instance ) {
updatePlayerMetadata( instance, { title, artist, image } );
}
}, [ title, artist, image ] );
const ref = useRefEffect(
( element ) => {
if ( ! src ) {
return;
}
let cancelled = false;
let playerDestroy;
function init() {
if ( cancelled ) {
return;
}
const player = initWaveformPlayer( element, {
src,
...metadataRef.current,
waveformStyle,
artist:
metadataRef.current.artist || EMPTY_ARTIST_PLACEHOLDER,
onEnded: () => onEndedEvent?.(),
} );
playerRef.current = player;
updatePlayerMetadata( player.instance, metadataRef.current );
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, src, waveformStyle, hasImage ]
);
return <div ref={ ref } className="wp-block-playlist__waveform-player" />;
}