@wordpress/block-library
Version:
Block library for the WordPress editor.
233 lines (212 loc) • 7.6 kB
JavaScript
/**
* Shared utilities for waveform audio player functionality.
* Used by both the WaveformPlayer component (editor) and view.js (frontend).
*/
/**
* External dependencies
*/
import { colord } from 'colord';
import WaveformPlayerLib from '@arraypress/waveform-player';
/**
* Configuration constants.
* Note: DEFAULT_WAVEFORM_HEIGHT should match $waveform-player-height in style.scss.
*/
const DEFAULT_WAVEFORM_HEIGHT = 100;
/**
* Get computed style for an element, using ownerDocument for iframe compatibility.
*
* @param {Element} element - The element to get styles from.
* @return {CSSStyleDeclaration} The computed style.
*/
function getComputedStyle( element ) {
return element.ownerDocument.defaultView.getComputedStyle( element );
}
/**
* Get all colors needed for the waveform player based on the element's styles.
*
* @param {Element} element - The element to derive colors from.
* @return {Object} Object containing textColor, waveformColor, progressColor.
*/
export function getWaveformColors( element ) {
const textColor = getComputedStyle( element ).color;
const waveformColor = colord( textColor ).alpha( 0.3 ).toRgbString();
const progressColor = colord( textColor ).alpha( 0.6 ).toRgbString();
return { textColor, waveformColor, progressColor };
}
/**
* Create a waveform container element with the specified attributes.
*
* @param {Object} options - The options for the container.
* @param {string} options.url - The audio URL.
* @param {string} options.title - The track title.
* @param {string} options.artist - The track artist.
* @param {string} options.artwork - The album artwork URL.
* @param {string} options.waveformColor - The waveform bar color.
* @param {string} options.progressColor - The progress indicator color.
* @param {string} options.buttonColor - The play button color.
* @param {number} options.height - The waveform height in pixels.
* @return {Element} The configured container element.
*/
export function createWaveformContainer( {
url,
title,
artist,
artwork,
waveformColor,
progressColor,
buttonColor,
height = DEFAULT_WAVEFORM_HEIGHT,
} ) {
const container = document.createElement( 'div' );
container.setAttribute( 'data-waveform-player', '' );
container.setAttribute( 'data-url', url );
container.setAttribute( 'data-height', String( height ) );
container.setAttribute( 'data-waveform-style', 'bars' );
container.setAttribute( 'data-waveform-color', waveformColor );
container.setAttribute( 'data-progress-color', progressColor );
container.setAttribute( 'data-button-color', buttonColor );
container.setAttribute( 'data-text-color', buttonColor );
container.setAttribute( 'data-text-secondary-color', buttonColor );
if ( title ) {
container.setAttribute( 'data-title', title );
}
if ( artist ) {
container.setAttribute( 'data-subtitle', artist );
}
if ( artwork ) {
container.setAttribute( 'data-artwork', artwork );
}
return container;
}
/**
* Apply contrasting color to SVG icon paths for visibility.
* The icons should contrast with the button background (which uses textColor).
*
* @param {Element} container - The waveform container element.
* @param {string} buttonColor - The button background color (textColor).
*/
export function styleSvgIcons( container, buttonColor ) {
// Compute a contrasting color for the icons based on button brightness.
const isButtonDark = colord( buttonColor ).isDark();
const iconColor = isButtonDark ? '#ffffff' : '#000000';
const svgPaths = container.querySelectorAll( 'svg path' );
svgPaths.forEach( ( path ) => {
path.style.fill = iconColor;
} );
}
/**
* Set up play button accessibility: aria-label that toggles on play/pause.
*
* @param {Element} container - The waveform container element.
* @param {Object} labels - Button labels.
* @param {string} labels.play - Label for the play state.
* @param {string} labels.pause - Label for the pause state.
*/
export function setupPlayButtonAccessibility(
container,
{ play: playLabel = 'Play', pause: pauseLabel = 'Pause' } = {}
) {
const playBtn = container.querySelector( '.waveform-btn' );
if ( ! playBtn ) {
return;
}
playBtn.setAttribute( 'aria-label', playLabel );
const onPlay = () => playBtn.setAttribute( 'aria-label', pauseLabel );
const onPause = () => playBtn.setAttribute( 'aria-label', playLabel );
container.addEventListener( 'waveformplayer:play', onPlay );
container.addEventListener( 'waveformplayer:pause', onPause );
container.addEventListener( 'waveformplayer:ended', onPause );
return () => {
container.removeEventListener( 'waveformplayer:play', onPlay );
container.removeEventListener( 'waveformplayer:pause', onPause );
container.removeEventListener( 'waveformplayer:ended', onPause );
};
}
/**
* Log play errors, filtering out expected AbortError.
*
* @param {Error} error - The error from play().
*/
export function logPlayError( error ) {
// The browser throws AbortError when a play() promise is interrupted
// by a subsequent pause() or a new audio source load (track change).
// This is normal during rapid user interaction and safe to ignore.
if ( error.name === 'AbortError' ) {
return;
}
// eslint-disable-next-line no-console
console.error( 'Playlist play error:', error );
}
/**
* Initialize a WaveformPlayer instance on an element.
*
* This is the shared core logic used by both the React component (editor)
* and the Interactivity API (frontend).
*
* @param {Element} element - The container element (must be in DOM).
* @param {Object} options - Configuration options.
* @param {string} options.src - The audio file URL.
* @param {string} options.title - The track title.
* @param {string} options.artist - The artist name.
* @param {string} options.image - The artwork image URL.
* @param {boolean} options.autoPlay - Whether to auto-play when ready.
* @param {Function} options.onEnded - Callback when track ends.
* @param {Object} options.labels - Translated button labels.
* @return {Object} Object with instance, container, and destroy function.
*/
export function initWaveformPlayer(
element,
{ src, title, artist, image, autoPlay, onEnded, labels }
) {
// Get colors from computed styles.
const { textColor, waveformColor, progressColor } =
getWaveformColors( element );
// Create the waveform container.
const container = createWaveformContainer( {
url: src,
title,
artist,
artwork: image,
waveformColor,
progressColor,
buttonColor: textColor,
} );
element.appendChild( container );
// Initialize the WaveformPlayer library.
const instance = new WaveformPlayerLib( container );
// Set up event handlers.
let cleanupAccessibility;
const handlers = {
ready: () => {
styleSvgIcons( container, textColor );
cleanupAccessibility = setupPlayButtonAccessibility(
container,
labels
);
if ( autoPlay ) {
instance.play()?.catch( logPlayError );
}
},
ended: () => onEnded?.(),
};
container.addEventListener( 'waveformplayer:ready', handlers.ready );
container.addEventListener( 'waveformplayer:ended', handlers.ended );
// Return instance, container, and cleanup function.
return {
instance,
container,
destroy: () => {
cleanupAccessibility?.();
container.removeEventListener(
'waveformplayer:ready',
handlers.ready
);
container.removeEventListener(
'waveformplayer:ended',
handlers.ended
);
instance.destroy();
container.remove();
},
};
}