Search. (VRFS-75)

This commit is contained in:
Jonathon Wilson 2012-11-12 21:12:17 -07:00
parent 4b392f8cde
commit 2fb617cab7
12 changed files with 518 additions and 17 deletions

View File

@ -126,9 +126,7 @@ Message from Seth on sequence for creating/joining sessions:
self.location = '#/session/' + newSessionId;
});
}
).fail(function(jqXHR, textStatus, errorMessage) {
app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText});
});
).fail(app.ajaxError);
return false;
}
@ -147,9 +145,7 @@ Message from Seth on sequence for creating/joining sessions:
data: invite
}).done(function(response) {
callCount--;
}).fail(function(jqXHR, textStatus, errorMessage) {
app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText});
});
}).fail(app.ajaxError);
});
// TODO - this is the second time I've used this pattern.
// refactor to make a common utility for this.

View File

@ -9,6 +9,7 @@
context.JK.Header = function(app) {
var logger = context.JK.logger;
var searcher; // Will hold an instance to a JK.Searcher (search.js)
var userMe = null;
var instrumentAutoComplete;
var instrumentIds = [];
@ -97,9 +98,7 @@
data: JSON.stringify(user)
}).done(function(response) {
userMe = response;
}).fail(function(jqXHR, textStatus, errorMessage) {
app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText});
});
}).fail(app.ajaxError);
return false;
}
@ -134,9 +133,7 @@
data: JSON.stringify(user)
}).done(function(response) {
userMe = response;
}).fail(function(jqXHR, textStatus, errorMessage) {
app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText});
});
}).fail(app.ajaxError);
return false;
}
@ -149,9 +146,7 @@
}).done(function(r) {
userMe = r;
updateAccountForms();
}).fail(function(jqXHR, textStatus, errorMessage) {
app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText});
});
}).fail(app.ajaxError);
}
function updateAccountForms() {
@ -178,6 +173,9 @@
events();
loadInstruments();
loadMe();
searcher = new JK.Searcher(app);
searcher.initialize();
};
};

View File

@ -76,6 +76,13 @@
context.JK.MessageType.LOGIN_ACK, loggedIn);
}
/**
* Generic error handler for Ajax calls.
*/
function ajaxError(jqXHR, textStatus, errorMessage) {
notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText});
}
/**
* Register for all known types, logging events as they happen, and
* notifying subscribers (see this.subscribe) as they occur.
@ -107,6 +114,11 @@
*/
this.fireEvent = handleMessage;
/**
* Expose ajaxError.
*/
this.ajaxError = ajaxError;
/**
* Provide a handler object for events related to a particular screen
* being shown or hidden.

View File

@ -27,7 +27,7 @@
client.JoinSession({ sessionID: session_id });
}
context.JK.refreshMusicSession(session_id);
});
}).fail(app.ajaxError);
};
})(window,jQuery);

View File

@ -0,0 +1,129 @@
(function(context,$) {
context.JK = context.JK || {};
context.JK.Searcher = function(app) {
var logger = context.JK.logger;
var searchSectionTemplate;
var searchItemTemplate;
var noResultsTemplate;
function events() {
$('.searchtextinput').keyup(handleKeyup);
$('.searchtextinput').focus(function(evt) {
var searchVal = $(this).val();
search(searchVal);
});
$('.searchtextinput').blur(hideSearchResults);
}
function templates() {
searchSectionTemplate = $('#template-search-section').html();
searchItemTemplate = $('#template-search-item').html();
noResultsTemplate = $('#template-search-noresults').html();
}
function hideSearchResults() {
$('.searchresults').hide();
}
function showSearchResults() {
$('.searchresults').show();
}
function handleKeyup(evt) {
if (evt.which === 27) {
return hideSearchResults();
}
var searchVal = $(this).val();
search(searchVal);
}
function search(query) {
if (query.length < 2) {
return;
}
$.ajax({
type: "GET",
url: "/api/search?query=" + query,
success: searchResponse,
error: app.ajaxError
});
}
function searchResponse(response) {
ensureResultsDiv();
updateResultsDiv(response);
positionResultsDiv();
showSearchResults();
}
function ensureResultsDiv() {
if ($('.searchresults').length === 0) {
$searchresults = $('<div/>');
$searchresults.addClass('searchresults');
$searchresults.css({position:'absolute'});
$('body').append($searchresults);
}
}
function updateResultsDiv(searchResults) {
var sections = ['musicians', 'bands', 'fans', 'recordings'];
var fullHtml = '';
$.each(sections, function() {
fullHtml += getSectionHtml(this, searchResults);
});
if (fullHtml === '') {
fullHtml += getNoResultsMessage();
}
$('.searchresults').html(fullHtml);
}
function getNoResultsMessage() {
// No replacement needed at the moment.
return noResultsTemplate;
}
function getSectionHtml(section, searchResults) {
if (section in searchResults && searchResults[section].length === 0) {
return '';
}
var items = '';
$.each(searchResults[section], function() {
items += getItemHtml(this);
});
var html = JK.fillTemplate(
searchSectionTemplate,
{ section: section, items: items });
return html;
}
function getItemHtml(item) {
var replacements = {
id: item.id,
name: item.first_name + " " + item.last_name,
image: item.photo_url,
subtext: item.location
};
return JK.fillTemplate(
searchItemTemplate, replacements);
}
function positionResultsDiv() {
var bodyOffset = $('body').offset();
var inputOffset = $('.searchtextinput').offset();
var inputHeight = $('.searchtextinput').outerHeight();
var resultsTop = bodyOffset.top + inputOffset.top + inputHeight;
var resultsLeft = bodyOffset.left + inputOffset.left;
$('.searchresults').css({
top: resultsTop + 'px',
left: resultsLeft + 'px'});
}
this.initialize = function() {
events();
templates();
};
};
})(window,jQuery);

View File

@ -0,0 +1,57 @@
/* Styles used by things related to search */
@import "client/common.css.scss";
/* Container for the search input */
.header .search {
position: absolute;
left: 50%;
margin-left: -125px;
top: 26px;
}
.searchtextinput {
border: 1px solid #fff;
background:none;
color:#fff;
font-size: 120%;
line-height: 120%;
width: 250px;
padding: 6px;
}
.searchresults {
background-color:$color8;
border:1px solid #000;
color:#000;
}
.searchresults h2 {
font-size: 120%;
font-weight: bold;
background-color: shade($color8, 10%);
}
.searchresults li {
clear:both;
padding: 8px;
}
.searchresults img {
float:left;
width: 32px;
height: 32px;
border: 1px solid #333;
margin: 4px;
}
.searchresults .text {
color: #000;
font-weight: bold;
}
.searchresults .subtext {
display:block;
color: #000;
font-size: 90%;
}

View File

@ -14,4 +14,4 @@ child(:fans => :fans) {
child(:recordings => :recordings) {
attributes :id, :name
}
}

View File

@ -1,6 +1,32 @@
<div class="curtain"></div>
<div layout="header" class="header">
<h1>JamKazam</h1>
<div class="search">
<input type="text" class="searchtextinput" placeholder="Search for Bands, Musicians and Fans"/>
</div>
<script type="text/template" id="template-search-section">
<h2>{section}</h2>
<ul>
{items}
</ul>
</script>
<script type="text/template" id="template-search-noresults">
<h2 class="emptyresult">No Matches</h2>
<p>No results returned</p>
</script>
<script type="text/template" id="template-search-item">
<li>
<a>
<img src="{image}"/>
<span class="text">{name}</span>
<span class="subtext">{subtext}</span>
</a>
</li>
</script>
<div class="userinfo">
<%= gravatar_for current_user, size: 52, hclass: "avatar medium" %>
<div class="username">

View File

@ -7,6 +7,7 @@
<![endif]-->
<%= stylesheet_link_tag "client/ie", media: "all" %>
<%= stylesheet_link_tag "client/jamkazam", media: "all" %>
<%= stylesheet_link_tag "client/search", media: "all" %>
<%= stylesheet_link_tag "client/lato", media: "all" %>
<%= include_gon %>
<%= javascript_include_tag "application" %>

View File

@ -0,0 +1,24 @@
<!-- Fixtures for Jasmine Tests for searcher.js -->
<input type="text" class="searchtextinput" placeholder="Search for Magical Things" />
<script type="text/template" id="template-search-section">
<h2>{section}</h2>
<ul>
{items}
</ul>
</script>
<script type="text/template" id="template-search-noresults">
<h2 class="emptyresult">No Matches</h2>
<p>No results returned</p>
</script>
<script type="text/template" id="template-search-item">
<li>
<a>
<img src="{image}"/>
<span class="text">{name}</span>
<span class="subtext">{subtext}</span>
</a>
</li>
</script>

View File

@ -0,0 +1,62 @@
window.TestResponses = {
search : {
bands: [ ],
musicians: [ ],
fans: [
{
id: "1",
first_name: "Test",
last_name: "User",
location: "Austin, TX",
photo_url: "http://www.jamkazam.com/images/users/photos/1.gif"
}
],
recordings: [ ]
},
emptySearch: {
bands: [],
musicians: [],
fans: [],
recordings: []
},
fullSearch: {
bands: [
{
id: "1",
first_name: "Test",
last_name: "User",
location: "Austin, TX",
photo_url: "http://www.jamkazam.com/images/users/photos/1.gif"
}
],
musicians: [
{
id: "1",
first_name: "Test",
last_name: "User",
location: "Austin, TX",
photo_url: "http://www.jamkazam.com/images/users/photos/1.gif"
}
],
fans: [
{
id: "1",
first_name: "Test",
last_name: "User",
location: "Austin, TX",
photo_url: "http://www.jamkazam.com/images/users/photos/1.gif"
}
],
recordings: [
{
id: "1",
first_name: "Test",
last_name: "User",
location: "Austin, TX",
photo_url: "http://www.jamkazam.com/images/users/photos/1.gif"
}
]
}
};

View File

@ -0,0 +1,196 @@
(function(context) {
describe("searcher.js tests", function() {
describe("Empty Search", function() {
// See the markup in fixtures/searcher.htm
var searcher;
var ajaxSpy;
var fakeApp = {
ajaxError: function() {
console.debug("ajaxError");
}
};
beforeEach(function() {
loadFixtures('searcher.htm');
spyOn($, "ajax").andCallFake(function(opts) {
opts.success(TestResponses.emptySearch);
});
searcher = new JK.Searcher(fakeApp);
searcher.initialize();
});
it("No Results message shown", function() {
// Workaround for key events not being reflected in val() calls
$('.searchtextinput').val('AA');
var e = jQuery.Event("keyup");
e.which = 65; // "a"
$('.searchtextinput').focus();
$('.searchtextinput').trigger(e);
$('.searchtextinput').trigger(e);
expect($('.searchresults .emptyresult').length).toEqual(1);
});
});
describe("Full Search", function() {
// See the markup in fixtures/searcher.htm
var searcher;
var ajaxSpy;
var fakeApp = {
ajaxError: function() {
console.debug("ajaxError");
}
};
beforeEach(function() {
loadFixtures('searcher.htm');
spyOn($, "ajax").andCallFake(function(opts) {
opts.success(TestResponses.fullSearch);
});
searcher = new JK.Searcher(fakeApp);
searcher.initialize();
});
it("No Results message shown", function() {
// Workaround for key events not being reflected in val() calls
$('.searchtextinput').val('AA');
var e = jQuery.Event("keyup");
e.which = 65; // "a"
$('.searchtextinput').focus();
$('.searchtextinput').trigger(e);
$('.searchtextinput').trigger(e);
expect($('.searchresults h2').length).toEqual(4);
});
});
describe("Search Tests", function() {
// See the markup in fixtures/searcher.htm
var searcher;
var ajaxSpy;
var fakeApp = {
ajaxError: function() {
console.debug("ajaxError");
}
};
beforeEach(function() {
loadFixtures('searcher.htm');
spyOn($, "ajax").andCallFake(function(opts) {
opts.success(TestResponses.search);
});
searcher = new JK.Searcher(fakeApp);
searcher.initialize();
});
it("first keypress should not search", function() {
// Workaround for key events not being reflected in val() calls
$('.searchtextinput').val('A');
var e = jQuery.Event("keyup");
e.which = 65; // "a"
$('.searchtextinput').focus();
$('.searchtextinput').trigger(e);
expect($.ajax.wasCalled).toBe(false);
});
it("second keypress should search", function() {
$('.searchtextinput').val('AA');
$('.searchtextinput').focus();
var e = jQuery.Event("keyup");
e.which = 65; // "a"
$('.searchtextinput').trigger(e);
// trigger again
$('.searchtextinput').trigger(e);
expect($.ajax).toHaveBeenCalled();
});
it("response div is absolute position", function() {
$('.searchtextinput').val('AA');
$('.searchtextinput').focus();
var e = jQuery.Event("keyup");
e.which = 65; // "a"
$('.searchtextinput').trigger(e);
$('.searchtextinput').trigger(e);
expect($('.searchresults').css('position')).toEqual('absolute');
});
it("response displayed in results", function() {
$('.searchtextinput').val('AA');
$('.searchtextinput').focus();
var e = jQuery.Event("keyup");
e.which = 65; // "a"
$('.searchtextinput').trigger(e);
$('.searchtextinput').trigger(e);
expect($('.searchresults').length).toEqual(1);
expect($('.searchresults h2').length).toEqual(1);
expect($('.searchresults li').length).toEqual(1);
expect($('.searchresults li img').length).toEqual(1);
expect($('.searchresults li span.text').length).toEqual(1);
expect($('.searchresults li span.subtext').length).toEqual(1);
});
it("response positioned under input", function() {
$('.searchtextinput').val('AA');
$('.searchtextinput').focus();
var e = jQuery.Event("keyup");
e.which = 65; // "a"
$('.searchtextinput').trigger(e);
$('.searchtextinput').trigger(e);
expect($('.searchresults').length).toEqual(1);
var bodyOffset = $('body').offset();
var inputOffset = $('.searchtextinput').offset();
var inputHeight = $('.searchtextinput').outerHeight();
var expectedTop = bodyOffset.top + inputOffset.top + inputHeight;
var expectedLeft = bodyOffset.left + inputOffset.left;
var searchResultOffset = $('.searchresults').offset();
expect(searchResultOffset.top).toEqual(expectedTop);
expect(searchResultOffset.left).toEqual(expectedLeft);
});
it("search results are visible", function() {
$('.searchtextinput').val('AA');
$('.searchtextinput').focus();
var e = jQuery.Event("keyup");
e.which = 65; // "a"
$('.searchtextinput').trigger(e);
$('.searchtextinput').trigger(e);
var visible = $('.searchresults').is(':visible');
expect(visible).toBe(true);
});
it("escape key hides search results", function() {
$('.searchtextinput').val('AA');
$('.searchtextinput').focus();
var e = jQuery.Event("keyup");
e.which = 65; // "a"
$('.searchtextinput').trigger(e);
$('.searchtextinput').trigger(e);
e = jQuery.Event("keyup");
e.which = 27; // ESCAPE
$('.searchtextinput').trigger(e);
var visible = $('.searchresults').is(':visible');
expect(visible).toBe(false);
});
});
});
})(window);