4
\$\begingroup\$

I have developed a tiny bit of JavaScript to quickly output a Froogaloop Vimeo video and some basic controls attached with almost no effort. You don't have to include the script from Vimeo (the script will add it for you) and it will create only the very basics of controls for now - although they can be amended. I also have a basic CSS (SASS) file that you can choose to omit as the output is simply a <ul> element you can style and nested <span> elements for scrubbers.

I was wondering what could be improved in this code. Is it a smart idea to not make this a Constructor but simply a function that you can keep calling? My thoughts were that you only want one controlling instance, although I guess it would be nice if you could programmatically undo your changes. Is it bad that there are no options or is it fine if people use CSS to hide the unneeded elements? Is the code well commented or is it lacking information? (For example, I wanted to comment on the DOM creating code, but I find it speaks for itself and adding anything would make it harder to read instead of clearer).

I cannot include a video here as Stack Overflow throws Cross origin errors, so I have not included the CSS below, but you can find more about this code at GitHub.

// Froogaloopelise(querySelectorLString) -> true;
// Initialise a request for a Froogaloopelise wrapper
function Froogaloopelise(querySelector){
  Froogaloopelise.addScript();
  // If the API has not yet loaded, create and add to queue
  if(!Froogaloopelise.scriptAdded){
    Froogaloopelise.queue.push(querySelector);
    return;
  }

  // Initialise iFrames
  for(
    var i = 0, iframes = document.querySelectorAll(querySelector); 
    i < iframes.length; 
    i++
  ){

    var iframe, id, src;
    iframe = iframes.item(i);
    // Set an id;
    id  = Froogaloopelise.getId(iframe);
    iframe.setAttribute('id', id);

    // Set the api and id for the player
    src = iframe.getAttribute('src');
    src = src.indexOf('?') >= 0 ? src + '&' : src + '?';
    src = src + 'api=1&player_id=' + id;

    iframe.setAttribute('src', src);

    // Create the meaty DOM
    iframe = Froogaloopelise.addUI(iframe, id);

  }
  return true;
}
// Basic Stored Variables
Froogaloopelise.id = 1; 
Froogaloopelise.scriptAdded = false;
Froogaloopelise.queue = [];

// Froogaloopelise.getId([element:<iframe>]) -> String
// Get a unique Id or return the already existing id of the element
Froogaloopelise.getId = function(element){
  return element && element.getAttribute('id') 
    ? element.getAttribute('id')
    : 'froogaloop-' + Froogaloopelise.id++;
};

// Froogaloopelise.executeQueue() -> Void
// Execute whatever is stacked in the queue since start
Froogaloopelise.executeQueue = function(){
  Froogaloopelise.queue.forEach(Froogaloopelise);
}

// Froogaloopelise.addScript() -> Void
// Add the froogaloop api script to the <head>, wait for it to load
Froogaloopelise.addScript = function(){

    if(Froogaloopelise.scriptAdded || document.getElementById('froogaloop-script')) 
        return false;

    var script          = document.createElement('script');
        script.id       = 'froogaloop-script';
        script.type     = 'text/javascript';
        script.src      = 'https://f.vimeocdn.com/js/froogaloop2.min.js';
        script.addEventListener('load', function(){
            Froogaloopelise.scriptAdded = true
            Froogaloopelise.executeQueue();
        }, false);

    document.head.appendChild(script);

}
// Froogaloopelise.FullScreenAPI
// Fullscreen API with prefixing.
Froogaloopelise.FullScreenAPI = {
  fullScreenElement: false,
  requestFulLScreen: function(element){
    if (element.requestFullscreen) {
      element.requestFullscreen();
    } else if (element.msRequestFullscreen) {
      element.msRequestFullscreen();
    } else if (element.mozRequestFullScreen) {
      element.mozRequestFullScreen();
    } else if (element.webkitRequestFullscreen) {
      element.webkitRequestFullscreen();
    } else {
      return false;
    }
    this.fullScreenElement = element;
    this.fullScreenElement.addEventListener('fullscreenchange', function(){
      this.fullScreenElement.removeAttribute('data-is-fullscreen');
      this.fullScreenElement = false;
    }.bind(this), false)
    this.fullScreenElement.setAttribute('data-is-fullscreen', 'true');
    return true;
  },
  exitFullScreen: function(){
    if (document.exitFullscreen) {
      document.exitFullscreen();
    } else if (document.msExitFullscreen) {
      document.msExitFullscreen();
    } else if (document.mozCancelFullScreen) {
      document.mozCancelFullScreen();
    } else if (document.webkitExitFullscreen) {
      document.webkitExitFullscreen();
    } else {
      return false;
    }
    return true;
  }
}
// Froogaloopelise.addUI(iframe:<node>) -> <node>
// Adds DOM elements and wraps into wrapper
Froogaloopelise.addUI = function(iframe){

  var wrapper, list, froogaloop, duration;

  wrapper = document.createElement('div');
  wrapper .className = '[ froogaloop froogaloop-wrapper ]'
  iframe  .parentNode.insertBefore(wrapper, iframe);
  iframe  .parentNode.removeChild(iframe);
  wrapper .appendChild(iframe);

  function resizeIframe(){
    iframe.width = wrapper.clientWidth;
    iframe.height = wrapper.clientWidth / 16 * 9;
  }

  window.addEventListener('resize', resizeIframe, false);
  resizeIframe();

  list = document.createElement('ul');
  wrapper .appendChild(list);

  // On froogaloop ready, add elements
  $f(iframe).addEvent('ready', function(id){
    froogaloop  = $f(id);
    froogaloop.api('getDuration', function(v){ duration = v; });
      var playpause;

      // Play/Pause Button
      playpause = document.createElement('li');
      playpause .className = '[ froogaloop play-pause state: paused ]';
      playpause .textContent = 'Play';
      playpause .addEventListener('click', function(){
      froogaloop.api('paused', function(paused){
        if(paused)
          froogaloop.api('play');
        else
          froogaloop.api('pause');
        });
      });
      list.appendChild(playpause);

      froogaloop.addEvent('play', function(){
        playpause.textContent = 'Pause';
        playpause.className = '[ froogaloop play-pause state: playing ]';
      });
      froogaloop.addEvent('pause', function(){
        playpause.textContent = 'Play';
        playpause.className = '[ froogaloop play-pause state: paused ]';
      });

      var rewind;

      rewind = document.createElement('li');
      rewind .className = '[ froogaloop rewind ]';
      rewind .textContent = 'Rewind';
      rewind .addEventListener('click', function(){
        froogaloop.api('seekTo', 0);
      });
      list.appendChild(rewind);

      var durationScrubber, durationScrubberPassed, durationMouseEvent;

      function durationScrubHandler(event){
        if(!durationMouseEvent || !duration || !durationScrubber) 
          return;
        var offsetParent = durationScrubber, 
        left = event.pageX - durationScrubber.offsetLeft;
        while(offsetParent.offsetParent){ 
          offsetParent = offsetParent.offsetParent;
          left -= offsetParent.offsetLeft;
        }
        froogaloop.api('seekTo', duration * left / durationScrubber.offsetWidth);
      }

      durationScrubber = document.createElement('li');
      durationScrubber .className = '[ froogaloop scrubber seek ]';
      durationScrubberPassed = document.createElement('span');
      durationScrubberPassed .className = '[ froogaloop scrubber seeker ]';
      durationScrubberPassed .textContent = 'Seek';
      durationScrubberPassed .style.width = '0%';
      durationScrubber .appendChild(durationScrubberPassed);
      durationScrubber .addEventListener('mousedown', function(event){ 
        durationMouseEvent = true;
        durationScrubHandler(event);
      }, false);
      durationScrubber .addEventListener('mouseup',     function(){ 
        durationMouseEvent = false; 
      }, false);
      durationScrubber .addEventListener('mouseleave',  function(){ 
        durationMouseEvent = false; 
      }, false);
      durationScrubber .addEventListener('mousemove', durationScrubHandler, false);
      list.appendChild(durationScrubber);

      froogaloop.addEvent('playProgress', function(data){
        durationScrubberPassed.style.width = (data.percent * 100) + '%';
      });

      var volumeScrubber, volumeScrubberPassed, volumeMouseEvent;

      function volumeScrubHandler(event){
        if(!volumeMouseEvent || !volumeScrubber) return;
        var offsetParent = volumeScrubber, 
        left = event.pageX - volumeScrubber.offsetLeft;
        while(offsetParent.offsetParent){ 
          offsetParent = offsetParent.offsetParent;
          left -= offsetParent.offsetLeft;
        }
        left = Math.round(left / volumeScrubber.offsetWidth * 10);
        volumeScrubberPassed.style.width = (left * 10) + '%';
        froogaloop.api('setVolume', left / 10);
      }

      volumeScrubber = document.createElement('li');
      volumeScrubber .className = '[ froogaloop scrubber volume ]';
      volumeScrubberPassed = document.createElement('span');
      volumeScrubberPassed .className = '[ froogaloop scrubber volume ]';
      volumeScrubberPassed .textContent = 'Volume';
      volumeScrubberPassed .style.width = '50%';
      volumeScrubber .appendChild(volumeScrubberPassed);
      volumeScrubber .addEventListener('mousedown', function(event){ 
        volumeMouseEvent = true;
        volumeScrubHandler(event);
      }, false);
      volumeScrubber .addEventListener('mouseup', function(){ 
        volumeMouseEvent = false; 
      }, false);
      volumeScrubber .addEventListener('mouseleave', function(){ 
        volumeMouseEvent = false; 
      }, false);
      volumeScrubber .addEventListener('mousemove', volumeScrubHandler, false);
      list.appendChild(volumeScrubber);

      var fullscreen, isFullScreen = false;

      // Play/Pause Button
      fullscreen = document.createElement('li');
      fullscreen .className = '[ froogaloop fullscreen ]';
      fullscreen .textContent = 'Fullscreen';
      fullscreen .addEventListener('click', function(){
        if(!isFullScreen && Froogaloopelise.FullScreenAPI.requestFulLScreen(wrapper))
          isFullScreen = wrapper;
        else if(isFullScreen && Froogaloopelise.FullScreenAPI.exitFullScreen(wrapper)) 
          isFullScreen = false;
      });
      list.appendChild(fullscreen);

    });

    return wrapper;
}
\$\endgroup\$
  • \$\begingroup\$ First thing I've noticed is you make this thing a constructor but then you invoke everything statically... this does not make sense. :P \$\endgroup\$ – Dan Pantry Sep 24 '15 at 11:59
  • \$\begingroup\$ @Aredherring it's not actually a constructor, its a function with methods attached to it but it cannot be instantiated using 'new'. You call it with Froogaloopelise('iframe') but every one of those is not actually an instance, just a conversion to the new UI. \$\endgroup\$ – somethinghere Sep 24 '15 at 12:01
  • \$\begingroup\$ Oh, excuse me, I was confused (because it was a capital function..) \$\endgroup\$ – Dan Pantry Sep 24 '15 at 12:01
  • \$\begingroup\$ @Aredherring I didn't actually know that was a convention, I have never really noticed. I usually capitalise functions that are not part of custom code but of libraries, so when using them it's obvious that you declared it yourself or imported it as a separate file. \$\endgroup\$ – somethinghere Sep 24 '15 at 12:03
  • 1
    \$\begingroup\$ @ARedHerring that's certainly a good way to do it. Something that I want to mention (I know, pernickety, but I keep hearing it) what you refer to is dromedaryCase (one bulge in the middle). This is CamelCase :) But thanks for the reply! (And don't take that thing about the case as critique, I just wanted to mention it :) ) \$\endgroup\$ – somethinghere Sep 24 '15 at 14:17
1
\$\begingroup\$

This isn't a comprehensive review, but a few things I noticed may be helpful:

There are a couple repetitive if else statements that can be condensed. For example, the following code:

   exitFullScreen: function(){
      if (document.exitFullscreen) {
        document.exitFullscreen();
      } else if (document.msExitFullscreen) {
        document.msExitFullscreen();
      } else if (document.mozCancelFullScreen) {
        document.mozCancelFullScreen();
      } else if (document.webkitExitFullscreen) {
        document.webkitExitFullscreen();
      } else {
        return false;
      }
      return true;
    }

can be condensed:

exitFullScreen: function() {
  var methodName = ['exitFullscreen', 'msExitFullscreen', 'mozExitFullScreen', 'webkitExitFullscreen']
                   .filter(function(x) { return document[x] })[0];
  return methodName ? (!!document[methodName]() || true) : false;
}

Note that the possibly cryptic (!!document[methodName]() || true) simply calls the defined method, and then returns true, regardless of what the method itself returns. The double bang !! simply coerces the returned value to a boolean.

The array recreation on each call shouldn't matter from a performance perspective, and is worth the conciseness. You might also consider separating out the detection of vendor prefix into a method that could be reused by all the pieces of code which branch on vendor.

As a more general note, the functions are long, which makes their overall purpose hard to grasp. A simple way to improve this is to move the sub-parts into named inner functions. This allows you to keep your comments in code (via well named functions), and also to separate high-level logic and implementation. Here's just one small section rewritten in this style:

// Now you can see an outline of what the code does all
// in one place, like a table of contents
addPlayPauseButton();
addPlayEventHandler();
addPauseEventHandler();

// And commented code sections are replaced with well-named functions
// encapsulating the implementation detail
function addPlayPauseButton() {
  // Play/Pause Button
  playpause = document.createElement('li');
  playpause .className = '[ froogaloop play-pause state: paused ]';
  playpause .textContent = 'Play';
  playpause .addEventListener('click', function(){
  froogaloop.api('paused', function(paused){
    // User ternary operator instead of 4 line if else
    paused ? froogaloop.api('play') : froogaloop.api('pause');
    });
  });
  list.appendChild(playpause);
}


function addPlayEventHandler() {
  froogaloop.addEvent('play', function(){
    playpause.textContent = 'Pause';
    playpause.className = '[ froogaloop play-pause state: playing ]';
  });
}

function addPauseEventHandler() {
  froogaloop.addEvent('pause', function(){
    playpause.textContent = 'Play';
    playpause.className = '[ froogaloop play-pause state: paused ]';
  });
}
\$\endgroup\$
  • \$\begingroup\$ Very good comments (: I was thinking of breaking it down into smaller functions as well, but decided against it. Then it spiralled out of control a bit... The if/else statements are indeed shorter and the vendor prefixed getfullscreen thing - I'll be honest, I nabbed that straight from MDN, but the way you are suggesting is much more legible. Thanks for the reply! I'm leaving the answering open for a little while longer just to see what will come out of it, but for now there's at least a +1! \$\endgroup\$ – somethinghere Sep 25 '15 at 6:04
  • 1
    \$\begingroup\$ Checking methodName !== undefined after doing document[methodName].call(); seems wrong. \$\endgroup\$ – 200_success Sep 25 '15 at 7:11
  • \$\begingroup\$ @200_success, thanks, good catch. There was another bug in the way I was using call, too. Both should be fixed now so that the behavior is the same as the original if else. \$\endgroup\$ – Jonah Sep 25 '15 at 13:53
  • \$\begingroup\$ Are you sure? If no method is found, it's supposed to return false. \$\endgroup\$ – 200_success Sep 25 '15 at 14:19
  • \$\begingroup\$ ugh, that one was a typo. thanks again. :embarrassed_face: \$\endgroup\$ – Jonah Sep 25 '15 at 14:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.