<!DOCTYPE html>
<html>

  <head>
    <script data-require="jquery@*" data-semver="2.1.1" src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <script data-require="bootstrap@*" data-semver="3.1.1" src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
    <script data-require="spin.js@*" data-semver="1.2.7" src="//cdnjs.cloudflare.com/ajax/libs/spin.js/1.2.7/spin.min.js"></script>
    <script src="loadpromise.js"></script>
    <script src="script.js"></script>
    <link data-require="bootstrap-css@3.1.1" data-semver="3.1.1" rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" />
    <link rel="stylesheet" href="style.css" />
  </head>

  <body>
    <div id="nav">
      <button class="btn btn-small btn-default" data-toggle="modal" data-target="#fileModal" title="Choose new image">
        <span class="glyphicon glyphicon-file"></span>
      </button>
    </div>
    <div class="modal fade" id="fileModal" tabindex="-1" role="dialog" aria-labelledby="fileModal" aria-hidden="true">
      <div class="modal-dialog">
        <div class="modal-content">
          <div class="modal-header">
            <button type="button" class="close" data-dismiss="modal" aria-hidden="true">
              <span class="glyphicon glyphicon-remove" title="Close"></span>
            </button>
            <h4 class="modal-title" id="myModalLabel">Choose your images</h4>
          </div>
          <div class="modal-body">
            <div id="input-container">
            
              <!-- <div class="input-group fieldwrapper" id="field1">
                <input type="text" class="form-control" placeholder="Insert image URL" id="url1" />
                <span class="input-group-btn">
                  <button type="button" class="btn btn-default">
                    <span class="glyphicon glyphicon-remove" title="Remove image"></span>
                  </button>
                </span>
              </div> -->
              
            </div>
            <div class="hide alert alert-warning" role="alert" id="fileModalAlert">
              <span class="message"></span>
              <a href="#" class="close" id="fileModalAlertClose">
                <small>
                  <span class="glyphicon glyphicon-remove small" title="Remove image"></span>
                </small>
              </a>
            </div>
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-default" title="Add Field" id="add-field">Add Field</button>
            <button type="button" class="btn btn-default" title="Cancel" data-dismiss="modal">Cancel</button>
            <button type="button" class="btn btn-primary" title="Load Images" id="loadImages">Load Images</button>
          </div>
        </div>
      </div>
    </div>
  </body>

</html>
$(document).on('ready', function(){

  // create image set
  var imageSet = $();
  
  // REGISTER EVENT HANDLERS

  $('#loadImages').on('click', function (e) {
    
    var imagesURLArray = [];
    
    // reset image set
    imageSet = $();
    
    // validate fields
    $('.input-group').each(function(index){
      
      var inputValue = $(this).find('input').val()
      
      if (inputValue === ""){
  			return true; // skip to next iteration. tolerate empty fields.
  		}

      // validate image
      if (isImageURL(inputValue)){
        
        // add http to url        
        var url = addHTTP(inputValue);
        
        // add url only if it's not a duplicate
        if ( imagesURLArray.indexOf(url) == -1)
          imagesURLArray.push(url);
        
      }else{
  			$('#fileModalAlert').removeClass('hide').addClass('alert alert-warning');
  			$('#fileModalAlert .message').text('There are invalid images');
  			
  			// focus and select the invalid input
  		 	$(this).find('input').focus().select();

  			imagesURLArray = null;
  			return false; // don't tolerate invalid images.
      }
    }); // end  validate fields .each
    
    if (imagesURLArray === null)
      return; // there are invalid images, leave
    
    // array is empty. no images added
    if (imagesURLArray.length < 1){
			$('#fileModalAlert').removeClass('hide').addClass('alert alert-warning');
			$('#fileModalAlert .message').text('Please add at least one image');
			imagesURLArray = null;
			return;
		}
    
    // now there's at least one image, and all are valid
    
    //var spinner = new Spinner({top: 0, left: 0}).spin(document.body);  // no jQuery
    
    // populate image set
    $.each(imagesURLArray, function(index, value) {
        imageSet = imageSet.add($("<img>").attr("src", value));
    });
    
    // load promise for each image on set
    imageSet.loadPromise().done(function() {
      // all images loaded
      //console.log('all ' + imageSet.length + ' image(s) loaded!');
      
      //spinner.stop();
      $('#fileModal').modal('hide');
  
      // now do something with the images.
      imageSet.each(function() {
        //console.log(this.width + ", " + this.height);
        //$('body').append("<p> url: " + this.src + "<br/> dimensions: " + this.width + ", " + this.height + "</p>");
      });
    });
  }); // end #loadImages on 'click'

  var addField = function(url){
    //console.log('adding field()');
    
    var intId = $("#input-container div").length + 1;
    var fieldWrapper = $("<div class=\"input-group fieldwrapper\" id=\"field" + intId + "\"/>");
    var fName = $("<input type=\"text\" class=\"form-control fieldname\" placeholder=\"Insert image URL\" />");
    var removeButton = $("<span class=\"input-group-btn\"><button type=\"button\" class=\"btn btn-default\"><span class=\"glyphicon glyphicon-remove\" title=\"Remove image\"></span></button></span>");
    
    removeButton.on('click', function() {
      // de not remove if it's the last one
      if ($('.input-group').length > 1) 
        $(this).parent().remove();
    });

    fieldWrapper.append(fName);
    fieldWrapper.append(removeButton);

    if (url !== undefined)
      fName.val(url);
      
    $("#input-container").append(fieldWrapper);

    // focus on last added input field
    // (doesn't work for the first input, guess it won't focus when the modal's hidden)

    if ($('#fileModal').hasClass('in')){
      fName.focus();  
    }else{
      // 'hack' 
      setTimeout( function(){ 
        $(".input-group:last-child","#input-container").find('input').focus();      
      }, 500 );
    }
  }

  // ON FILE MODAL SHOW
  
  $('#fileModal').on('show.bs.modal', function (e) {
    //console.log('SHOW MODAL');
    
    // remove all fields
    $('.input-group').remove();
    
    imageSet.each(function(index) {
      addField(this.src);
    });

    // hide alert case it's opened
    $('#fileModalAlert').addClass('hide');
    
    // add then one empty field (and focus on it)
    addField();
  });
  
  // ADD FIELD HANDLER
  
  $("#add-field").on('click', function(e){
    addField();
  });
  
  // ALERT CLOSE HANDLER

  $('#fileModalAlertClose').on('click', function(){
    $('#fileModalAlert').addClass('hide');
  });

});

function addHTTP(url) {
   if (!/^(f|ht)tps?:\/\//i.test(url))
      url = "http://" + url;
   return url;
}

function isImageURL(url) {
    return(url.match(/\.(jpeg|jpg|gif|png)$/) !== null);
}

// this might be useful for urls that return an image
// http://stackoverflow.com/questions/9714525/javascript-image-url-verify
// function testImage(url, callback, timeout) {
//     timeout = timeout || 5000;
//     var timedOut = false, timer;
//     var img = new Image();
//     img.onerror = img.onabort = function() {
//         if (!timedOut) {
//             clearTimeout(timer);
//             callback(url, "error");
//         }
//     };
//     img.onload = function() {
//         if (!timedOut) {
//             clearTimeout(timer);
//             callback(url, "success");
//         }
//     };
//     img.src = url;
//     timer = setTimeout(function() {
//         timedOut = true;
//         callback(url, "timeout");
//     }, timeout); 
// }
html, body { height:100%; margin: 0; padding: 10px; }

.input-group {
  margin-bottom:10px;
}
issues:
  
1) These should be together:

    localStorage.setItem('myImageURLWidth', this.width);
    localStorage.setItem('myImageURLHeight', this.height);
    
    and
    
    localStorage.setItem('myImageURL', url);
    
    ...but aren't, because to know image's width and height, the image must be loaded before hand. And it wouldn't make sense to pass the image's url along for the callback function (updateBackgroundDimensions) to use it.
  
2) For the same reason,

  updateBackgroundImage(url);
  
  and 
  
  updateBackgroundDimensions();
  
  aren't together as well
  
3) How could I separate all the LocalStorage related code (ex: checkURL() and addHTTP()) into a module or something? That includes methods like:
(function() {
    // hook up a dummy animation that completes when the image finishes loading
    // If you call this before doing an animation, then animations will
    // wait for the image to load before starting
    // This does nothing for elements in the collection that are not images
    // It can be called multiple times with no additional side effects
    var waitingKey = "_waitingForLoad";
    var doneEvents = "load._waitForLoad error._waitForLoad abort._waitForLoad";
        
    jQuery.fn.waitForLoad = function() {
        return this.each(function() {
            var self = $(this);
            if (this.tagName && this.tagName.toUpperCase() === "IMG" && 
              !this.complete && !self.data(waitingKey)) {
                self.queue(function(next) {
                    // nothing to do here as this is just a sentinel that 
                    // triggers the start of the fx queue
                    // it will get called immediately and will put the queue "inprogress"
                }).on(doneEvents, function() {
                    // remove flag that we're waiting,
                    // remove event handlers
                    // and finish the pseudo animation
                    self.removeData(waitingKey).off(doneEvents).dequeue();
                }).data(waitingKey, true);
            }
        });
    };

    jQuery.fn.loadPromise = function() {
        this.waitForLoad();
        return this.promise();
    };
})();