Upgraded result navigation (v3)

* New results per page button
* New request selected button
* Finished select checkboxes behavior
* Fixed result count text
* Changed colors
* Added dynamic filter categories
* Added recursive retrieval for unknown length data sets

Change-Id: I50e023b2b073eae29ceba4203d3ad682de75c427
diff --git a/client/catalog/css/style.css b/client/catalog/css/style.css
index d784b53..e9593b3 100644
--- a/client/catalog/css/style.css
+++ b/client/catalog/css/style.css
@@ -37,10 +37,6 @@
   border: none;
 }
 
-.pager {
-  /* margin: 0; */
-}
-
 #searchBar {
   display: inline-block;
 }
@@ -104,12 +100,27 @@
   position: absolute;
   left: 0;
   transition: max-height 1s;
+  z-index: 1;
 }
 
 *:focus ~ .autoComplete { /* If the parent detects focus on any subelement or itself */
   max-height: 500px;
 }
 
-.pager li > div {
-  display: inline-block;
+.interest-button {
+  font-size: medium;
+}
+
+.interest-button.disabled {
+  cursor: not-allowed !important;
+  color: gray;
+}
+
+.hidden {
+  height: 0;
+  display: none;
+}
+
+.disabled {
+  cursor: not-allowed;
 }
diff --git a/client/catalog/index.html b/client/catalog/index.html
index abb3e05..61e4215 100644
--- a/client/catalog/index.html
+++ b/client/catalog/index.html
@@ -42,7 +42,7 @@
     <div class="row">
 
       <div class="col-sm-3 col-md-2 sidebar">
-        <div class="panel panel-default">
+        <div class="panel panel-primary">
           <div class="panel-heading">
             <span>Filter Categories</span>
           </div>
@@ -56,7 +56,7 @@
 
         <div id="alerts"></div>
 
-        <div class="panel panel-default">
+        <div class="panel panel-info">
           <div class="panel-heading">Filter Based Search</div>
           <div class="panel-body">
             <div id="filters"></div>
@@ -64,7 +64,7 @@
           </div>
         </div>
 
-        <div class="panel panel-default">
+        <div class="panel panel-info">
           <div class="panel-heading">Path Based Search</div>
           <div class="panel-body">
             <form class="form-inline" id="searchBar">
@@ -82,24 +82,53 @@
           </div>
         </div>
 
-        <div class="panel panel-primary">
-          <div class="panel-heading">Results</div>
+        <div id="results" class="panel panel-default hidden">
           <div class="panel-body">
-            <nav>
-              <ul class="pager">
-                <li class="previous disabled"><a href="#">&larr; Previous</a></li>
-                <li>(Page <div class="pageNumber">0</div>) Showing <span class="pageLength">25</span> of <div class="totalResults">0</div> Results</li>
-                <li class="next disabled"><a href="#">Next &rarr;</a></li>
+            <nav class="navbar navbar-inverse col-md-12 resultMenu hidden">
+              <ul class="nav navbar-nav navbar-left">
+                <li class="previous disabled">
+                  <a href="#">&larr; Previous</a>
+                </li>
+                <li class="dropdown">
+                  <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button">Results Per Page <span class="caret"></span></a>
+                  <ul class="dropdown-menu">
+                    <li class="active"><a href="#" class="resultsPerPageSelector">25</a></li>
+                    <li><a href="#" class="resultsPerPageSelector">50</a></li>
+                    <li><a href="#" class="resultsPerPageSelector">100</a></li>
+                    <li><a href="#" class="resultsPerPageSelector">200</a></li>
+                  </ul>
+                </li>
+                <li><a href="#" class="requestSelectedButton">Request Selected</a></li>
+              </ul>
+              <div class="navbar-text">(Page <span class="pageNumber">0</span>) <span class="pageLength">0</span>/<span class="totalResults">0</span> Results</div>
+              <ul class="nav navbar-nav navbar-right">
+                <li class="next disabled">
+                  <a href="#">Next &rarr;</a>
+                </li>
               </ul>
             </nav>
             <table id="resultTable" class="table"></table>
-          </div>
-          <div class="panel-footer">
-            <nav>
-              <ul class="pager">
-                <li class="previous disabled"><a href="#">&larr; Previous</a></li>
-                <li>(Page <div class="pageNumber">0</div>) Showing <span class="pageLength">25</span> of <div class="totalResults">0</div> Results</li>
-                <li class="next disabled"><a href="#">Next &rarr;</a></li>
+            <nav class="navbar navbar-inverse col-md-12 resultMenu hidden">
+              <ul class="nav navbar-nav navbar-left">
+                <li class="previous disabled">
+                  <a href="#">&larr; Previous</a>
+                </li>
+                <li class="dropup">
+                  <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button">Results Per Page <span class="caret"></span></a>
+                  <ul class="dropdown-menu">
+                    <li class="active"><a href="#" class="resultsPerPageSelector">25</a></li>
+                    <li><a href="#" class="resultsPerPageSelector">50</a></li>
+                    <li><a href="#" class="resultsPerPageSelector">100</a></li>
+                    <li><a href="#" class="resultsPerPageSelector">200</a></li>
+                  </ul>
+                </li>
+                <li><a href="#" class="requestSelectedButton">Request Selected</a></li>
+              </ul>
+              <div class="navbar-text">(Page <span class="pageNumber">0</span>) <span class="pageLength">0</span>/<span class="totalResults">0</span> Results</div>
+              <ul class="nav navbar-nav navbar-right">
+                <li class="next disabled">
+                  <a href="#">Next &rarr;</a>
+                </li>
               </ul>
             </nav>
           </div>
diff --git a/client/catalog/js/catalog.js b/client/catalog/js/catalog.js
index d3bde8f..2136e4f 100644
--- a/client/catalog/js/catalog.js
+++ b/client/catalog/js/catalog.js
@@ -62,80 +62,24 @@
     this.searchInput = $('#search');
     this.searchBar = $('#searchBar');
     this.searchButton = $('#searchButton');
-    this.pagers = $('.pager');
+    this.resultMenu = $('.resultMenu');
     this.alerts = $('#alerts');
 
     var scope = this;
 
     this.resultTable.on('click', '.interest-button', function(){
-      var button = $(this);
-
-      if (button.is(':disabled')){
-        console.warn("Attempt to request again!");
-      }
-
-      var name = button.parent().prev().text();
-      var interest = new Interest(new Name('/retrieve' + name));
-      scope.face.expressInterest(interest, function(){}, function(){});
-
-      button.text("Requested!")
-        .removeClass('btn-primary')
-        .addClass('btn-success')
-        .addClass('disabled')
-        .prop('disabled', true);
+      scope.request(this);
     });
 
-    //Filter setup
-    $.getJSON("search_catagories.json").done(function (data) {
-      $.each(data, function (pageSection, contents) {
-        if (pageSection == "SearchCatagories") {
-          $.each(contents, function (category, searchOptions) {
-            //Create the category
-            var e = $('<li><a href="#">' + category.replace(/\_/g, " ") + '</a><ul class="subnav nav nav-pills nav-stacked"></ul></li>');
-
-            var sub = e.find('ul.subnav');
-            $.each(searchOptions, function(index, name){
-              //Create the filter list inside the category
-              var item = $('<li><a href="#">' + name + '</a></li>');
-              sub.append(item);
-              item.click(function(){ //Click on the side menu filters
-                if (item.hasClass('active')){ //Does the filter already exist?
-                  item.removeClass('active');
-                  scope.filters.find(':contains(' + category + ':' + name + ')').remove();
-                } else { //Add a filter
-                  item.addClass('active');
-                  var filter = $('<span class="label label-default"></span>');
-                  filter.text(category + ':' + name);
-
-                  scope.filters.append(filter);
-
-                  filter.click(function(){ //Click on a filter
-                    filter.remove();
-                    item.removeClass('active');
-                  });
-                }
-
-              });
-            });
-
-            //Toggle the menus. (Only respond when the immediate tab is clicked.)
-            e.find('> a').click(function(){
-              scope.categories.find('.subnav').slideUp();
-              var t = $(this).siblings('.subnav');
-              if ( !t.is(':visible') ){ //If the sub menu is not visible
-                t.slideDown(function(){
-                  t.triggerHandler('focus');
-                }); //Make it visible and look at it.
-              }
-            });
-
-            scope.categories.append(e);
-
-          });
-        }
-      });
+    $('.requestSelectedButton').click(function(){
+      scope.request(
+        scope.resultTable.find('.resultSelector:checked:not([disabled])')
+        .parent().next().find('.interest-button')
+      );
     });
 
+    this.filterSetup();
+
     this.searchInput.autoComplete(function(field, callback){
       scope.autoComplete(field, callback);
     });
@@ -150,21 +94,30 @@
       scope.search();
     });
 
-    this.pagers.find('.next').click(function(){
+    this.resultMenu.find('.next').click(function(){
       if (!$(this).hasClass('disabled')){
         scope.getResults(scope.page + 1);
       }
     });
-    this.pagers.find('.previous').click(function(){
+    this.resultMenu.find('.previous').click(function(){
       if (!$(this).hasClass('disabled')){
         scope.getResults(scope.page - 1);
       }
     });
-    this.pagers.find('.pageLength').attr('contentEditable', true)
-    .blur(function(){
-      scope.resultsPerPage = Number($(this).text());
-      scope.pagers.find('.pageLength').text(scope.resultsPerPage);
-      scope.getResults(0); //Reset page to 0;
+
+    var rpps = $('.resultsPerPageSelector').click(function(){
+
+      var t = $(this);
+
+      if (t.hasClass('active')){
+        return;
+      }
+
+      rpps.find('.active').removeClass('active');
+      t.addClass('active');
+      scope.resultsPerPage = Number(t.text());
+      scope.getResults(0); //Force return to page 1;
+
     });
 
   }
@@ -283,29 +236,47 @@
 
   Atmos.prototype.showResults = function(resultIndex) {
 
+    if ($('#results').hasClass('hidden')){
+      $('#results').removeClass('hidden').slideDown();
+    }
+
     var results = this.results.slice(this.resultsPerPage * resultIndex, this.resultsPerPage * (resultIndex + 1));
 
-    var resultDOM = $(results.reduce(function(prev, current){
-      prev.push('<tr><td><input type="checkbox"></td><td>');
-      prev.push(current);
-      prev.push('</td><td><button class="interest-button btn btn-primary btn-sm">Retrieve</button></td></tr>');
-      return prev;
-    }, []).join(''));
+    var resultDOM = $(
+      results.reduce(function(prev, current){
+        prev.push('<tr><td><input class="resultSelector" type="checkbox"></td><td>');
+        prev.push(current);
+        prev.push('</td></tr>');
+        return prev;
+      }, ['<tr><th><input id="resultSelectAll" type="checkbox" title="Select All"> Select</th><th>Name</th></tr>']).join('')
+    );
 
-    this.resultTable.empty().append(resultDOM);
+    resultDOM.find('#resultSelectAll').click(function(){
+      if ($(this).is(':checked')){
+        resultDOM.find('.resultSelector:not([disabled])').prop('checked', true);
+      } else {
+        resultDOM.find('.resultSelector:not([disabled])').prop('checked', false);
+      }
+    });
 
-    this.pagers.find('.pageNumber').text(resultIndex + 1);
+    this.resultTable.empty().append(resultDOM).slideDown();
+    if (this.resultMenu.hasClass('hidden')){
+      this.resultMenu.removeClass('hidden').slideDown();
+    }
+
+    this.resultMenu.find('.pageNumber').text(resultIndex + 1);
+    this.resultMenu.find('.pageLength').text(this.resultsPerPage * (resultIndex + 1));
 
     if (this.resultsPerPage * (resultIndex + 1) >= this.resultCount) {
-      this.pagers.find('.next').addClass('disabled');
+      this.resultMenu.find('.next').addClass('disabled');
     } else if (resultIndex === 0){
-      this.pagers.find('.next').removeClass('disabled');
+      this.resultMenu.find('.next').removeClass('disabled');
     }
 
     if (resultIndex === 0){
-      this.pagers.find('.previous').addClass('disabled');
+      this.resultMenu.find('.previous').addClass('disabled');
     } else if (resultIndex === 1) {
-      this.pagers.find('.previous').removeClass('disabled');
+      this.resultMenu.find('.previous').removeClass('disabled');
     }
 
   }
@@ -338,8 +309,8 @@
       function(interest, data){ //Response
 
         if (data.getContent().length === 0){
-          scope.pagers.find('.totalResults').text(0);
-          scope.pagers.find('.pageNumber').text(0);
+          scope.resultMenu.find('.totalResults').text(0);
+          scope.resultMenu.find('.pageNumber').text(0);
           console.log("Empty response.");
           return;
         }
@@ -347,8 +318,8 @@
         var content = JSON.parse(data.getContent().toString().replace(/[\n\0]/g,""));
 
         if (!content.results){
-          scope.pagers.find('.totalResults').text(0);
-          scope.pagers.find('.pageNumber').text(0);
+          scope.resultMenu.find('.totalResults').text(0);
+          scope.resultMenu.find('.pageNumber').text(0);
           console.log("No results were found!");
           return;
         }
@@ -357,7 +328,7 @@
 
         scope.resultCount = content.resultCount;
 
-        scope.pagers.find('.totalResults').text(scope.resultCount);
+        scope.resultMenu.find('.totalResults').text(scope.resultCount);
 
         scope.page = index;
 
@@ -417,6 +388,136 @@
 
     this.alerts.append(alert);
   }
+
+  /**
+   * Requests all of the names represented by the buttons in the elements list.
+   *
+   * @param elements {Array<jQuery>} A list of the interestButton elements
+   */
+  Atmos.prototype.request = function(elements){
+
+    var scope = this;
+    $(elements).filter(':not(.disabled)').each(function(){
+      var button = $(this);
+
+      if (button.hasClass('disabled')){
+        console.warn("An attempt to request a disabled element has occured");
+        return;
+      }
+
+      var name = button.text();
+      var interest = new Interest(new Name('/retrieve' + name));
+      scope.face.expressInterest(interest, function(){}, function(){});
+
+    })
+    .append('<span class="badge">Requested!</span>')
+    .addClass('disabled')
+    .addClass('label-success')
+    .parent().prev().find('.resultSelector').prop('disabled', true).prop('checked', false);
+
+  }
+
+  Atmos.prototype.filterSetup = function() {
+    //Filter setup
+
+    var prefix = new Name(this.catalog).append("filters-initialization");
+
+    var scope = this;
+
+    this.getAll(prefix, function(data) { //Success
+      var raw = JSON.parse(data.replace(/[\n\0]/g, '')); //Remove null byte and parse
+
+      console.log("Filter categories:", raw);
+
+      $.each(raw, function(index, object){ //Unpack list of objects
+        $.each(object, function(category, searchOptions) { //Unpack category from object (We don't know what it is called)
+          //Create the category
+          var e = $('<li><a href="#">' + category.replace(/\_/g, " ") + '</a><ul class="subnav nav nav-pills nav-stacked"></ul></li>');
+
+          var sub = e.find('ul.subnav');
+          $.each(searchOptions, function(index, name){
+            //Create the filter list inside the category
+            var item = $('<li><a href="#">' + name + '</a></li>');
+            sub.append(item);
+            item.click(function(){ //Click on the side menu filters
+              if (item.hasClass('active')){ //Does the filter already exist?
+                item.removeClass('active');
+                scope.filters.find(':contains(' + category + ':' + name + ')').remove();
+              } else { //Add a filter
+                item.addClass('active');
+                var filter = $('<span class="label label-default"></span>');
+                filter.text(category + ':' + name);
+
+                scope.filters.append(filter);
+
+                filter.click(function(){ //Click on a filter
+                  filter.remove();
+                  item.removeClass('active');
+                });
+              }
+
+            });
+          });
+
+          //Toggle the menus. (Only respond when the immediate tab is clicked.)
+          e.find('> a').click(function(){
+            scope.categories.find('.subnav').slideUp();
+            var t = $(this).siblings('.subnav');
+            if ( !t.is(':visible') ){ //If the sub menu is not visible
+              t.slideDown(function(){
+                t.triggerHandler('focus');
+              }); //Make it visible and look at it.
+            }
+          });
+
+          scope.categories.append(e);
+
+        });
+      });
+
+    }, function(interest){ //Timeout
+      scope.createAlert("Failed to initialize the filters!", "alert-danger");
+      console.error("Failed to initialize filters!", interest);
+    });
+
+  }
+
+  Atmos.prototype.getAll = function(prefix, callback, timeout){
+
+    var scope = this;
+    var d = [];
+
+    var request = function(segment){
+
+      var name = new Name(prefix);
+      name.appendSegment(segment);
+
+      var interest = new Interest(name);
+      interest.setInterestLifetimeMilliseconds(1000);
+      interest.setMustBeFresh(true); //Is this needed?
+
+      scope.face.expressInterest(interest, handleData, timeout);
+
+    }
+
+
+    var handleData = function(interest, data){
+
+      d.push(data.getContent().toString());
+
+      if (interest.getName().get(-1).toSegment() == data.getMetaInfo().getFinalBlockId().toSegment()){
+        callback(d.join(""));
+      } else {
+        request(interest.getName().toSegment()++);
+      }
+
+    }
+
+    request(0);
+
+
+  }
+
   Atmos.closeButton = '<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>';
 
   return Atmos;