( function() { function formatTime( totalSeconds ) { if ( ! Number.isFinite( totalSeconds ) || totalSeconds < 0 ) { return '0:00'; } const minutes = Math.floor( totalSeconds / 60 ); const seconds = Math.floor( totalSeconds % 60 ); return minutes + ':' + String( seconds ).padStart( 2, '0' ); } function initPlayers( root ) { root.querySelectorAll( '[data-map-player]' ).forEach( initPlayer ); } function initPlayer( root ) { if ( root.dataset.mapInitialized === 'true' ) { return; } const audio = root.querySelector( '[data-map-audio]' ); const toggle = root.querySelector( '[data-map-toggle]' ); const previousButton = root.querySelector( '[data-map-previous]' ); const nextButton = root.querySelector( '[data-map-next]' ); const repeatButton = root.querySelector( '[data-map-repeat]' ); const progress = root.querySelector( '[data-map-progress]' ); const currentTime = root.querySelector( '[data-map-current]' ); const duration = root.querySelector( '[data-map-duration]' ); const title = root.querySelector( '[data-map-title]' ); const coverWrap = root.querySelector( '.map-player__cover-wrap' ); const cover = root.querySelector( '[data-map-cover]' ); const toggleText = root.querySelector( '.map-player__toggle-text' ); const editorNotice = root.querySelector( '[data-map-editor-notice]' ); const isEditorPreview = root.dataset.mapEditorPreview === 'true'; const showCoverSetting = root.dataset.mapShowCoverSetting === 'true'; const trackedHashes = new Set(); let isRepeatEnabled = root.dataset.mapRepeatEnabled === 'true'; let currentIndex = Number( root.dataset.mapCurrentIndex || 0 ); let playlist = []; if ( ! audio || ! toggle || ! progress || ! currentTime || ! duration ) { return; } try { const parsedPlaylist = JSON.parse( root.dataset.mapPlaylist || '[]' ); if ( Array.isArray( parsedPlaylist ) ) { playlist = parsedPlaylist.filter( function( track ) { return track && track.src; } ); } } catch ( error ) { playlist = []; } if ( ! playlist.length && root.dataset.trackSrc ) { playlist = [ { src: root.dataset.trackSrc, title: root.dataset.trackTitle || 'Untitled Track', image: cover && cover.getAttribute( 'src' ) ? cover.getAttribute( 'src' ) : '', hash: root.dataset.trackHash || '', nonce: root.dataset.trackNonce || '' } ]; } if ( ! playlist.length ) { return; } currentIndex = Math.max( 0, Math.min( currentIndex, playlist.length - 1 ) ); root.dataset.mapInitialized = 'true'; function getCurrentTrack() { return playlist[ currentIndex ] || playlist[ 0 ]; } function setEditorNotice( message ) { if ( ! editorNotice ) { return; } editorNotice.textContent = message; } function syncTrackDataset( track ) { root.dataset.mapCurrentIndex = String( currentIndex ); root.dataset.trackSrc = track.src || ''; root.dataset.trackTitle = track.title || ''; root.dataset.trackHash = track.hash || ''; root.dataset.trackNonce = track.nonce || ''; } function updateCoverUI( track ) { const shouldShowCover = showCoverSetting && !! track.image; root.classList.toggle( 'map-player--no-cover', ! shouldShowCover ); root.dataset.mapHasCover = shouldShowCover ? 'true' : 'false'; if ( ! coverWrap || ! cover ) { return; } coverWrap.style.display = shouldShowCover ? '' : 'none'; if ( shouldShowCover ) { cover.src = track.image; cover.alt = track.title || 'Audio cover'; } } function updateTransportState() { const atFirstTrack = currentIndex === 0; const atLastTrack = currentIndex === playlist.length - 1; if ( previousButton ) { previousButton.disabled = atFirstTrack; } if ( nextButton ) { nextButton.disabled = atLastTrack; } if ( repeatButton ) { repeatButton.classList.toggle( 'is-active', isRepeatEnabled ); repeatButton.setAttribute( 'aria-pressed', isRepeatEnabled ? 'true' : 'false' ); } } function updateProgressUI() { if ( ! Number.isFinite( audio.duration ) || audio.duration <= 0 ) { progress.value = 0; duration.textContent = '0:00'; currentTime.textContent = '0:00'; progress.style.setProperty( '--map-progress-percent', '0%' ); return; } const percent = ( audio.currentTime / audio.duration ) * 100; progress.value = percent; progress.style.setProperty( '--map-progress-percent', percent + '%' ); currentTime.textContent = formatTime( audio.currentTime ); duration.textContent = formatTime( audio.duration ); } function setPlayingState( isPlaying ) { root.classList.toggle( 'is-playing', isPlaying ); toggle.setAttribute( 'aria-pressed', isPlaying ? 'true' : 'false' ); toggle.setAttribute( 'aria-label', isPlaying ? 'Pause audio' : 'Play audio' ); toggleText.textContent = isPlaying ? 'Pause' : 'Play'; } function applyTrack( options ) { const track = getCurrentTrack(); const autoplay = !! ( options && options.autoplay ); if ( title ) { title.textContent = track.title || 'Untitled Track'; } updateCoverUI( track ); updateTransportState(); syncTrackDataset( track ); if ( audio.getAttribute( 'src' ) !== track.src ) { audio.setAttribute( 'src', track.src ); } if ( audio.src !== track.src ) { audio.src = track.src; } audio.currentTime = 0; audio.load(); updateProgressUI(); if ( autoplay ) { audio.play().catch( function() { if ( isEditorPreview ) { setEditorNotice( 'Playback preview is limited in the editor. Test full playback on the frontend.' ); } } ); } } function ensureAudioLoaded() { const track = getCurrentTrack(); if ( ! track || ! track.src ) { return; } if ( audio.getAttribute( 'src' ) !== track.src ) { applyTrack( { autoplay: false } ); return; } if ( audio.networkState === HTMLMediaElement.NETWORK_EMPTY || ! Number.isFinite( audio.duration ) || audio.duration <= 0 ) { try { audio.load(); } catch ( error ) { return; } } } function trackPlay() { const track = getCurrentTrack(); if ( ! track || ! track.src || ! track.hash || ! track.nonce || ! root.dataset.restEndpoint || trackedHashes.has( track.hash ) ) { return; } trackedHashes.add( track.hash ); window.fetch( root.dataset.restEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify( { src: track.src, title: track.title || '', hash: track.hash, nonce: track.nonce } ), credentials: 'same-origin' } ).catch( function() { return null; } ); } function switchTrack( nextIndex, options ) { if ( nextIndex < 0 || nextIndex >= playlist.length || nextIndex === currentIndex ) { return; } currentIndex = nextIndex; applyTrack( options ); } toggle.addEventListener( 'click', function() { ensureAudioLoaded(); if ( audio.paused ) { audio.play().catch( function() { if ( isEditorPreview ) { setEditorNotice( 'Playback preview is limited in the editor. Test full playback on the frontend.' ); } } ); return; } audio.pause(); } ); if ( previousButton ) { previousButton.addEventListener( 'click', function() { if ( previousButton.disabled ) { return; } if ( audio.currentTime > 5 ) { audio.currentTime = 0; updateProgressUI(); return; } switchTrack( currentIndex - 1, { autoplay: ! audio.paused } ); } ); } if ( nextButton ) { nextButton.addEventListener( 'click', function() { if ( nextButton.disabled ) { return; } switchTrack( currentIndex + 1, { autoplay: ! audio.paused } ); } ); } if ( repeatButton ) { repeatButton.addEventListener( 'click', function() { isRepeatEnabled = ! isRepeatEnabled; root.dataset.mapRepeatEnabled = isRepeatEnabled ? 'true' : 'false'; updateTransportState(); } ); } audio.addEventListener( 'play', function() { setPlayingState( true ); trackPlay(); } ); audio.addEventListener( 'pause', function() { setPlayingState( false ); } ); audio.addEventListener( 'loadedmetadata', updateProgressUI ); audio.addEventListener( 'durationchange', updateProgressUI ); audio.addEventListener( 'canplay', updateProgressUI ); audio.addEventListener( 'timeupdate', updateProgressUI ); audio.addEventListener( 'error', function() { updateProgressUI(); setEditorNotice( 'Unable to load audio preview. Confirm the URL points directly to an audio file.' ); } ); audio.addEventListener( 'ended', function() { if ( isRepeatEnabled ) { audio.currentTime = 0; audio.play().catch( function() { return null; } ); return; } if ( currentIndex < playlist.length - 1 ) { switchTrack( currentIndex + 1, { autoplay: true } ); return; } setPlayingState( false ); audio.currentTime = 0; updateProgressUI(); } ); progress.addEventListener( 'input', function() { if ( ! Number.isFinite( audio.duration ) || audio.duration <= 0 ) { return; } const targetTime = ( Number( progress.value ) / 100 ) * audio.duration; audio.currentTime = targetTime; updateProgressUI(); } ); applyTrack( { autoplay: false } ); } function observePlayerMounts() { if ( ! document.body || typeof MutationObserver === 'undefined' ) { return; } const observer = new MutationObserver( function( mutations ) { mutations.forEach( function( mutation ) { mutation.addedNodes.forEach( function( node ) { if ( ! node || node.nodeType !== 1 ) { return; } if ( node.matches && node.matches( '[data-map-player]' ) ) { initPlayer( node ); return; } if ( node.querySelectorAll ) { initPlayers( node ); } } ); } ); } ); observer.observe( document.body, { childList: true, subtree: true } ); } function boot() { initPlayers( document ); observePlayerMounts(); } if ( document.readyState === 'loading' ) { document.addEventListener( 'DOMContentLoaded', boot ); } else { boot(); } } )();