Search code examples
javascriptjqueryhtmlshow-hidecustom-data-attribute

Show a target div on audio play with a selector retrieved via a custom data attribute


I have an audio player with several songs. For each song, I want to display a div below that has the lyrics to that specific song, while hiding the lyrics for all the other songs.

This is the script for the audio player, which works:

initAudio($('#playlist li:first-child'));

function initAudio(element){
    var song = element.attr('song');
    var title = element.text();
    var cover = element.attr('cover');
    var artist = element.attr('artist');
    var album = element.attr('album');

    //Create audio object
    audio = new Audio('public/songs/'+ song);

    //Insert audio info
    $('.artist').text(artist);
    $('.title').text(title);
    $('.album').text(album);

    //Insert song cover
    $('img.cover').attr('src','public/images/' + cover);

    $('#playlist li').removeClass('active');
    element.addClass('active');

    // Set timer to 0
    $('#duration').html('0:00');

    // Maintain volume on song change
    audio.volume = $('#volume').val() / 100;
}

And here is a simplified version of the playlist and lyrics HTML:

<ul id="playlist">
    <li class="song-list active" song="song-1.wav" data-id="song-1"></li>
    <li class="song-list" song="song-2.wav" data-id="song-2"></li>
    <li class="song-list" song="song-3.wav" data-id="song-3"></li>
</ul>

<div id="song-1" class="lyrics">Lyrics to song 1</div>
<div id="song-2" class="lyrics">Lyrics to song 2</div>
<div id="song-3" class="lyrics">Lyrics to song 3</div>

I tried a lot of things, to no avail. I'm not even sure how to target each song as it plays to extract the data-attribute.

Any ideas?


Solution

  • Note: For the next steps we assume to target "song-1".

    1. Get the data-id-attribute by calling element.attr('data-id');
      returns string "song-1".
    2. Target the DOM element with id="song-1" by calling $('#song-1');
      which would be $('#' + element.attr('data-id')) (take a look at step 1 if you don't know why).
    3. Now that we have our target we should hide all other lyrics in advance $('.lyrics').hide() and only display the wanted one $('#' + element.attr('data-id')).show().

    initAudio($('#playlist li:first-child'));
    
    function initAudio( element ) {
        // this version performs slightly better than the one in the 1, 2, 3 explanations
        $('.lyrics').hide().filter('#' + element.attr('data-id')).show()
    }
    
    $('#playlist').on('click', function( oEvent ) {
        initAudio( $( oEvent.target ) )
    } );
    <ul id="playlist">
        <li class="song-list active" song="song-1.wav" data-id="song-1">Click Me 1!</li>
        <li class="song-list" song="song-2.wav" data-id="song-2">Click Me 2!</li>
        <li class="song-list" song="song-3.wav" data-id="song-3">Click Me 3!</li>
    </ul>
    
    <div id="song-1" class="lyrics">Lyrics to song 1</div>
    <div id="song-2" class="lyrics">Lyrics to song 2</div>
    <div id="song-3" class="lyrics">Lyrics to song 3</div>
    
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

    This works like you want it to (I hope so - at least) but isn't an ideal solution; just wanted to explain whats going on! The downside about the above solution is a lot of unnecessary computing.

    So here is an advanced mockup (with less computing):

    //initAudio($('#playlist li:first-child'));
    initAudio($('#playlist').find('li:first-child')); // performs better
    
    function initAudio( element ) {
        // if you chain it this way you don't have to cache $('.lyrics')
        $('.lyrics') // get collection of all div.lyrics
            .filter('.active').removeClass('active') // target the current div.lyrics.active and remove class "active"
                .end() // get collection div.lyrics back again
                    .filter('#' + element.attr('data-id')).addClass('active'); // filter target div.lyrics#song-[1|2|3] and add class "active"
    }
    
    $('#playlist').on('click', function( oEvent ) {
        initAudio( $( oEvent.target ) )
    } );
    .lyrics { /* this will prevent to show all lyrics before you call initAudio() */
        display: none;
    }
    .lyrics.active { /* style active lyrics as you like */
        display: initial;
    }
    <ul id="playlist">
        <li class="song-list active" song="song-1.wav" data-id="song-1">Click Me 1!</li>
        <li class="song-list" song="song-2.wav" data-id="song-2">Click Me 2!</li>
        <li class="song-list" song="song-3.wav" data-id="song-3">Click Me 3!</li>
    </ul>
    
    <!-- set one as active in advance if you like -->
    <div id="song-1" class="lyrics active">Lyrics to song 1</div>
    <div id="song-2" class="lyrics">Lyrics to song 2</div>
    <div id="song-3" class="lyrics">Lyrics to song 3</div>
    
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

    Additional Recommondation

    To keep valid markup while using custom HTML5 attributes simply prepend your attributes with data-.

    So consider to change

    song="song-#.wav"
    

    to

    data-song="song-#.wav"
    

    and your markup will be valid. (The same for the other attributes as well cover to data-cover, artist to data-artist, etc.).


    References for jQuery methods that are not used in OP


    Additional Inspiration

    Depending on how you build your site there would be also more straight forward solutions than storing data into markup with data-*-attributes.

    Why not separating the data from the actual HTML. For doing so you could deploy the data as a variable.

    var sIdCurrent = undefined,
        oData = {
        'song-1': { artist: 'artist 1', lyrics: 'Lyrics to song 1' },
        'song-2': { artist: 'artist 2', lyrics: 'Lyrics to song 2' },
        'song-3': { artist: 'artist 3', lyrics: 'Lyrics to song 3' },
    };
    
    function initAudio( sId ) {
    
        if ( sId === sIdCurrent ) return;
        sIdCurrent = sId;
        //$('#artist').find('span').text( oData[ sId ].artist );
        //$('#lyrics').find('span').text( oData[ sId ].lyrics );
        // the above can be abstracted to
        for ( sKey in oData[ sId ] ) {
            $('#' + sKey).find('span').text( oData[ sId ][ sKey ] );
        }
    
    }
    
    initAudio('song-1');
    
    $('#playlist').on('click', function( oEvent ) {
        initAudio( oEvent.target.id )
    } );
    <!-- As you can see the markup is much cleaner now.
         No need to store data uneffectively in markup. -->
    
    <ul id="playlist">
        <li class="song-list active" id="song-1">Click Me 1!</li>
        <li class="song-list" id="song-2">Click Me 2!</li>
        <li class="song-list" id="song-3">Click Me 3!</li>
    </ul>
    
    <div id="artist">Artist: <span><span></div>
    <div id="lyrics">Lyrics: <span><span></div>
    
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>