UNPKG

@wordpress/block-library

Version:
233 lines (212 loc) 7.6 kB
/** * 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(); }, }; }