A fitVids vanilla JavaScript do-over

FitVids.js is a jQuery plugin used to create fluid videos. It helps makes video embeds from YouTube, Vimeo and a number of other sources display nicely on responsive sites.

FitVids calculates the ratio of a video, wraps it in a div and sets the padding to enforce a ratio. A typical 4:3 YouTube embed starts as:

<iframe 
  width="420" height="315" 
  src="//www.youtube.com/embed/btPJPFnesV4" 
  frameborder="0" allowfullscreen></iframe>

After FitVids.js runs, it becomes:

<style>
.fluid-width-video-wrapper{
  width: 100%;
  position: relative;
  padding: 0;
}

.fluid-width-video-wrapper iframe {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
</style>

<div class="fluid-width-video-wrapper" 
  style="padding-top: 75%;">
<iframe 
  src="//www.youtube.com/embed/btPJPFnesV4" 
  frameborder="0" allowfullscreen ></iframe>
</div>

The top padding on the wrapper element forces its height to be the correct ratio. The video is then absolutely positioned within the wrapper element. As Dave Rupert explains, it’s based on Thierry Koblentz’s intrinsic ratios.

My first do-over of FitVids.js was to remove the requirement for jQuery. I really like jQuery -I use it almost daily- but document.querySelectorAll is adequate for the purpose.

(function( document ) {
  var selectors = [
      'iframe[src*="player.vimeo.com"]',
      'iframe[src*="youtube.com"]'
      ],
      $allVids, $vid,
      $wrap, id,
      i,l,
      width,height,ratio;

  if (!('querySelectorAll' in document )) {
    // does not cut mustard
    return;
  }

  $allVids = document.querySelectorAll( selectors.join(',') );

  for ( i=0, l=$allVids.length; i<l; i++ ) {
    $vid = $allVids[i];
 
    // calculate ratio
    width = $vid.width;
    height = $vid.height;
    ratio = height / width;
 
    // create wrapper element
    $wrap = document.createElement("div");
    $wrap.id = 'fluid-video-wrapper-' + i;
    $wrap.className = "fluid-width-video-wrapper";
    $wrap.style.paddingTop = (ratio * 100) + "%";

    // replace video with wrapper
    $vid.parentNode.replaceChild($wrap, $vid);
    // put video within wrapper
    $vid.removeAttribute("width");
    $vid.removeAttribute("height");
    $wrap.appendChild($vid);
  }
}( window.document ));

The CSS remains unchanged.

Ensuring fitvids fit in the browser window.

While dumping the requirement for jQuery is a good thing, it only solves half the problem. There is nothing to prevent the embed from being too tall for the viewport. The video wrapper needs a maximum height of 100vh.

.fluid-width-video-wrapper {
  max-height: 100vh;
}

This would be a cinch, but the wrapper’s height is zero. The space it fills comes from the value of padding-top. Attempting to set a max-height on either the video or the wrapper causes a dog’s breakfast of disproportioned or cropped video.

My next attempt was to set a max-width on the wrapper based on the viewport’s height. The maximum width becomes 100vh divided by the ratio. No dice; the video is now cropped narrow, as the wrapper’s padding-top percentage is based upon the wrapper’s parent’s width.

Which brings us to the :before element. Pseudo elements are child elements, so percentage dimensions can be based on the width of the wrapper element. This allows a maximum width to be set on the wrapper element in vh, and the padding applied to the before element. For a 4:3 video, the css becomes:

.fluid-width-video-wrapper {
  width: 100%;
  position: relative;
  padding: 0;
  max-width: 133.33vh; /* 100vh / (3/4) */
}

.fluid-width-video-wrapper:before {
  padding-top: 75%; /* 3/4 */
  display: block;
  content: ' ';
}

.fluid-width-video-wrapper iframe {
  /* unchanged from above */
}

The code has the effect off setting the video’s maximum height to 100% of that of the viewport. One can’t simply style a before element using JavaScript, so a dynamic style element needs to be created. The fitVids.js redux becomes:

(function( document, undefined ) {
  var selectors = [
      'iframe[src*="player.vimeo.com"]',
      'iframe[src*="youtube.com"]'
      ],
      $allVids, $vid,
      $wrap, id,
      i,l,
      width,height,ratio,
      styles = [],
      $style;

  if (!('querySelectorAll' in document )) {
    // does not cut mustard
    return;
  }

  $allVids = document.querySelectorAll( selectors.join(',') );
  for ( i=0, l=$allVids.length; i<l; i++ ) {
    $vid = $allVids[i];
 
    // calculate ratio
    width = $vid.width;
    height = $vid.height;
    ratio = height / width;
 
    // create wrapper element
    $wrap = document.createElement("div");
    $wrap.id = 'fluid-video-wrapper-' + i;
    $wrap.className = "fluid-width-video-wrapper";
    // $wrap.style.paddingTop = (ratio * 100) + "%";
    pushRules( $wrap.id, ratio );
 
    // add wrapped video to document
    $vid.removeAttribute("width");
    $vid.removeAttribute("height");
    $wrap.appendChild($vid.cloneNode(true));
    $vid.parentNode.replaceChild($wrap, $vid);
  }
 
  $style = document.createElement( 'style' );
  document.head.appendChild($style);
 
  if ( $style.styleSheet ) {
    $style.styleSheet.cssText = styles.join( "\n" );
  }
  else {
    $style.innerHTML = styles.join( "\n" );
  }
 
  function pushRules( wrapId, ratio ) {
    var selector = '#'+ wrapId,
        paddingTop = (ratio * 100) + "%",
        maxWidth = (95/ratio) + 'vh';
 
    styles.push( selector + '{max-width:' + maxWidth + '}' );
    styles.push( selector + ':before{padding-top:' + paddingTop + '}' );
  }
}( window.document ));

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.