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="#">← 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 →</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="#">← 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 →</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="#">← 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 →</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="#">← 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 →</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">×</span></button>';
return Atmos;