Replace jquery.autocomplete with typeahead.js
This commit is contained in:
parent
25be9ecfd2
commit
8bc86eb98b
18 changed files with 252 additions and 1002 deletions
1
Gemfile
1
Gemfile
|
|
@ -104,6 +104,7 @@ source "https://rails-assets.org" do
|
|||
gem "rails-assets-markdown-it-sub", "1.0.0"
|
||||
gem "rails-assets-markdown-it-sup", "1.0.0"
|
||||
gem "rails-assets-highlightjs", "8.6.0"
|
||||
gem "rails-assets-typeahead.js", "0.11.1"
|
||||
|
||||
# jQuery plugins
|
||||
|
||||
|
|
|
|||
|
|
@ -569,6 +569,8 @@ GEM
|
|||
rails-assets-markdown-it-sub (1.0.0)
|
||||
rails-assets-markdown-it-sup (1.0.0)
|
||||
rails-assets-perfect-scrollbar (0.6.4)
|
||||
rails-assets-typeahead.js (0.11.1)
|
||||
rails-assets-jquery (>= 1.7)
|
||||
rails-deprecated_sanitizer (1.0.3)
|
||||
activesupport (>= 4.2.0.alpha)
|
||||
rails-dom-testing (1.0.6)
|
||||
|
|
@ -860,6 +862,7 @@ DEPENDENCIES
|
|||
rails-assets-markdown-it-sub (= 1.0.0)!
|
||||
rails-assets-markdown-it-sup (= 1.0.0)!
|
||||
rails-assets-perfect-scrollbar (= 0.6.4)!
|
||||
rails-assets-typeahead.js (= 0.11.1)!
|
||||
rails-i18n (= 4.0.4)
|
||||
rails-timeago (= 2.11.0)
|
||||
rails_admin (= 0.6.8)
|
||||
|
|
|
|||
|
|
@ -6,11 +6,6 @@ app.views.Header = app.views.Base.extend({
|
|||
|
||||
className: "dark-header",
|
||||
|
||||
events: {
|
||||
"focusin #q": "toggleSearchActive",
|
||||
"focusout #q": "toggleSearchActive"
|
||||
},
|
||||
|
||||
presenter: function() {
|
||||
return _.extend({}, this.defaultPresenter(), {
|
||||
podname: gon.appConfig.settings.podname
|
||||
|
|
@ -24,13 +19,5 @@ app.views.Header = app.views.Base.extend({
|
|||
},
|
||||
|
||||
menuElement: function(){ return this.$("ul.dropdown"); },
|
||||
|
||||
toggleSearchActive: function(evt){
|
||||
// jQuery produces two events for focus/blur (for bubbling)
|
||||
// don't rely on which event arrives first, by allowing for both variants
|
||||
var isActive = (_.indexOf(["focus","focusin"], evt.type) !== -1);
|
||||
$(evt.target).toggleClass("active", isActive);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
// @license-end
|
||||
|
|
|
|||
|
|
@ -1,71 +1,95 @@
|
|||
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
|
||||
app.views.Search = app.views.Base.extend({
|
||||
events: {
|
||||
"focusin #q": "toggleSearchActive",
|
||||
"focusout #q": "toggleSearchActive",
|
||||
"keypress #q": "inputKeypress",
|
||||
},
|
||||
|
||||
initialize: function(){
|
||||
this.searchFormAction = this.$el.attr('action');
|
||||
this.searchInput = this.$('input[type="search"]');
|
||||
this.searchInputName = this.$('input[type="search"]').attr('name');
|
||||
this.searchInputHandle = this.$('input[type="search"]').attr('handle');
|
||||
this.options = {
|
||||
cacheLength: 15,
|
||||
delay: 800,
|
||||
extraParams: {limit: 4},
|
||||
formatItem: this.formatItem,
|
||||
formatResult: this.formatResult,
|
||||
max: 5,
|
||||
minChars: 2,
|
||||
onSelect: this.selectItemCallback,
|
||||
parse: this.parse,
|
||||
scroll: false,
|
||||
context: this
|
||||
};
|
||||
this.searchFormAction = this.$el.attr("action");
|
||||
this.searchInput = this.$("#q");
|
||||
|
||||
var self = this;
|
||||
this.searchInput.autocomplete(self.searchFormAction + '.json',
|
||||
$.extend(self.options, { element: self.searchInput }));
|
||||
// constructs the suggestion engine
|
||||
this.setupBloodhound();
|
||||
this.setupTypeahead();
|
||||
this.searchInput.on("typeahead:select", this.suggestionSelected);
|
||||
},
|
||||
|
||||
formatItem: function(row){
|
||||
if(typeof row.search !== 'undefined') { return Diaspora.I18n.t('search_for', row); }
|
||||
else {
|
||||
var item = '';
|
||||
if (row.avatar) { item += '<img src="' + row.avatar + '" class="avatar"/>'; }
|
||||
item += row.name;
|
||||
if (row.handle) { item += '<div class="search_handle">' + row.handle + '</div>'; }
|
||||
return item;
|
||||
}
|
||||
},
|
||||
|
||||
formatResult: function(row){ return Handlebars.Utils.escapeExpression(row.name); },
|
||||
|
||||
parse: function(data) {
|
||||
var self = this.context;
|
||||
|
||||
var results = data.map(function(person){
|
||||
person.name = self.formatResult(person);
|
||||
return {data : person, value : person.name};
|
||||
});
|
||||
|
||||
results.push({
|
||||
data: {
|
||||
name: self.searchInput.val(),
|
||||
url: self.searchFormAction + '?' + self.searchInputName + '=' + self.searchInput.val(),
|
||||
search: true
|
||||
setupBloodhound: function() {
|
||||
this.bloodhound = new Bloodhound({
|
||||
datumTokenizer: function(datum) {
|
||||
var nameTokens = Bloodhound.tokenizers.nonword(datum.name);
|
||||
var handleTokens = datum.handle ? Bloodhound.tokenizers.nonword(datum.name) : [];
|
||||
return nameTokens.concat(handleTokens);
|
||||
},
|
||||
value: self.searchInput.val()
|
||||
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
||||
remote: {
|
||||
url: this.searchFormAction + ".json?q=%QUERY",
|
||||
wildcard: "%QUERY",
|
||||
transform: this.transformBloodhoundResponse
|
||||
},
|
||||
prefetch: {
|
||||
url: "/contacts.json",
|
||||
transform: this.transformBloodhoundResponse,
|
||||
cache: false
|
||||
},
|
||||
sufficient: 5
|
||||
});
|
||||
|
||||
return results;
|
||||
},
|
||||
|
||||
selectItemCallback: function(evt, data, formatted){
|
||||
var self = this.context;
|
||||
setupTypeahead: function() {
|
||||
this.searchInput.typeahead({
|
||||
hint: false,
|
||||
highlight: true,
|
||||
minLength: 2
|
||||
},
|
||||
{
|
||||
name: "search",
|
||||
display: "name",
|
||||
limit: 5,
|
||||
source: this.bloodhound,
|
||||
templates: {
|
||||
/* jshint camelcase: false */
|
||||
suggestion: HandlebarsTemplates.search_suggestion_tpl
|
||||
/* jshint camelcase: true */
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
if(data.search === true){
|
||||
window.location = self.searchFormAction + '?' + self.searchInputName + '=' + data.name;
|
||||
}
|
||||
else{ // The actual result
|
||||
self.options.element.val(formatted);
|
||||
window.location = data.url ? data.url : '/tags/' + data.name.substring(1);
|
||||
transformBloodhoundResponse: function(response) {
|
||||
var result = response.map(function(data) {
|
||||
// person
|
||||
if(data.handle) {
|
||||
data.person = true;
|
||||
return data;
|
||||
}
|
||||
|
||||
// hashtag
|
||||
return {
|
||||
hashtag: true,
|
||||
name: data.name,
|
||||
url: Routes.tag(data.name.substring(1))
|
||||
};
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
toggleSearchActive: function(evt) {
|
||||
// jQuery produces two events for focus/blur (for bubbling)
|
||||
// don't rely on which event arrives first, by allowing for both variants
|
||||
var isActive = (_.indexOf(["focus","focusin"], evt.type) !== -1);
|
||||
$(evt.target).toggleClass("active", isActive);
|
||||
},
|
||||
|
||||
suggestionSelected: function(evt, datum) {
|
||||
window.location = datum.url;
|
||||
},
|
||||
|
||||
inputKeypress: function(evt) {
|
||||
if(evt.which === 13 && $(".tt-suggestion.tt-cursor").length === 0) {
|
||||
$(evt.target).closest("form").submit();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
//= require jakobmattsson-jquery-elastic
|
||||
//= require jquery.mentionsInput
|
||||
//= require jquery.infinitescroll-custom
|
||||
//= require jquery.autocomplete-custom
|
||||
//= require jquery-ui/core
|
||||
//= require jquery-ui/widget
|
||||
//= require jquery-ui/mouse
|
||||
|
|
@ -35,6 +34,7 @@
|
|||
//= require markdown-it-sup
|
||||
//= require highlightjs
|
||||
//= require clear-form
|
||||
//= require typeahead.js
|
||||
//= require app/app
|
||||
//= require diaspora
|
||||
//= require_tree ./helpers
|
||||
|
|
|
|||
|
|
@ -7,14 +7,6 @@ var View = {
|
|||
/* label placeholders */
|
||||
$("input, textarea").placeholder();
|
||||
|
||||
/* "Toggling" the search input */
|
||||
$(this.search.selector)
|
||||
.blur(this.search.blur)
|
||||
.focus(this.search.focus)
|
||||
|
||||
/* Submit the form when the user hits enter */
|
||||
.keypress(this.search.keyPress);
|
||||
|
||||
/* Dropdowns */
|
||||
$(document)
|
||||
.on('click', this.dropdowns.selector, this.dropdowns.click)
|
||||
|
|
@ -48,16 +40,6 @@ var View = {
|
|||
});
|
||||
},
|
||||
|
||||
search: {
|
||||
blur: function() {
|
||||
$(this).removeClass("active");
|
||||
},
|
||||
focus: function() {
|
||||
$(this).addClass("active");
|
||||
},
|
||||
selector: "#q"
|
||||
},
|
||||
|
||||
dropdowns: {
|
||||
click: function(evt) {
|
||||
$(this).parent('.dropdown').toggleClass("active");
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
/* core */
|
||||
@import 'media-box';
|
||||
@import 'autocomplete';
|
||||
@import 'entypo';
|
||||
@import 'icons';
|
||||
@import 'mentions';
|
||||
|
|
@ -21,6 +20,7 @@
|
|||
@import 'timeago';
|
||||
@import 'vendor/fileuploader';
|
||||
@import 'vendor/autoSuggest';
|
||||
@import 'typeahead';
|
||||
|
||||
/* font overrides */
|
||||
@import 'new_styles/typography';
|
||||
|
|
|
|||
|
|
@ -1,95 +0,0 @@
|
|||
.ac_results {
|
||||
border: 1px solid #999;
|
||||
background-color: transparent;
|
||||
overflow: hidden;
|
||||
z-index: 99999;
|
||||
min-width: 300px !important;
|
||||
width: 100%;
|
||||
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 3px #999;
|
||||
}
|
||||
|
||||
.ac_results ul {
|
||||
width: 100%;
|
||||
list-style-position: outside;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ac_results li {
|
||||
color: white;
|
||||
margin: 0px;
|
||||
padding: 2px 5px {
|
||||
left: 50px;
|
||||
top: 6px;
|
||||
}
|
||||
cursor: default;
|
||||
display: block;
|
||||
height: 45px;
|
||||
position: relative;
|
||||
// if width will be 100% horizontal scrollbar will apear
|
||||
// when scroll mode will be used
|
||||
// width 100%
|
||||
font: menu;
|
||||
font-size: 1em;
|
||||
// it is very important, if line-height not setted or setted
|
||||
// in relative units scroll will be broken in firefox
|
||||
//:line-height 16px
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ac_input + .spinner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ac_input.ac_loading + .spinner {
|
||||
display: inline-block;
|
||||
height: 18px;
|
||||
margin-left: -26px;
|
||||
margin-right: 8px;
|
||||
margin-top: 7px;
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.ac_odd {
|
||||
background-color: $navbar-inverse-bg;
|
||||
}
|
||||
|
||||
.ac_even {
|
||||
background-color: darken($navbar-inverse-bg, 3%);
|
||||
}
|
||||
|
||||
.ac_over {
|
||||
background-color: $brand-primary;
|
||||
}
|
||||
|
||||
.ac_results {
|
||||
.avatar {
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.search_handle {
|
||||
font-size: 0.8em;
|
||||
color: #999;
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
.ac_over .search_handle{
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ac_over .search_handle, .search_handle {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
30
app/assets/stylesheets/typeahead.scss
Normal file
30
app/assets/stylesheets/typeahead.scss
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
.tt-menu {
|
||||
width: 300px;
|
||||
margin-top: 11px;
|
||||
background-color: $navbar-inverse-bg;
|
||||
box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
||||
}
|
||||
|
||||
.tt-suggestion {
|
||||
border-top: 1px solid $gray-dark;
|
||||
color: $white;
|
||||
cursor: pointer;
|
||||
line-height: 20px;
|
||||
&.tt-cursor {
|
||||
background-color: $brand-primary;
|
||||
}
|
||||
|
||||
&.search-suggestion-person {
|
||||
padding: 8px;
|
||||
.avatar {
|
||||
height: 40px;
|
||||
margin-right: 8px;
|
||||
width: 40px;
|
||||
}
|
||||
.diaspora-id { font-size: $font-size-small; }
|
||||
}
|
||||
&.search-suggestion-hashtag {
|
||||
padding: 8px 20px;
|
||||
.name { line-height: 25px; }
|
||||
}
|
||||
}
|
||||
|
|
@ -101,8 +101,7 @@
|
|||
|
||||
<form id="header-search-form" accept-charset="UTF-8" action="/search" class="navbar-form navbar-right" role="search" method="get">
|
||||
<div class="form-group">
|
||||
<input id="q" name="q" placeholder="{{t "header.search"}}" results="5" type="search" autocomplete="off" class="ac_input form-control input-sm">
|
||||
<div class="spinner"></div>
|
||||
<input id="q" name="q" placeholder="{{t "header.search"}}" results="5" type="search" autocomplete="off" class="form-control input-sm">
|
||||
</div>
|
||||
<input name="utf8" type="hidden" value="✓">
|
||||
</form>
|
||||
|
|
|
|||
13
app/assets/templates/search_suggestion_tpl.jst.hbs
Normal file
13
app/assets/templates/search_suggestion_tpl.jst.hbs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{{#if person}}
|
||||
<div class="search-suggestion-person">
|
||||
{{#if avatar}}
|
||||
<img src="{{ avatar }}" class="avatar pull-left">
|
||||
{{/if}}
|
||||
<div class="name">{{ name }}</div>
|
||||
<div class="diaspora-id">{{ handle }}</div>
|
||||
</div>
|
||||
{{else}}{{#if hashtag}}
|
||||
<div class="search-suggestion-hashtag">
|
||||
<div class="name">{{ name }}</div>
|
||||
</div>
|
||||
{{/if}}{{/if}}
|
||||
|
|
@ -40,6 +40,7 @@
|
|||
"_",
|
||||
"autosize",
|
||||
"Backbone",
|
||||
"Bloodhound",
|
||||
"gon",
|
||||
"Handlebars",
|
||||
"HandlebarsTemplates",
|
||||
|
|
|
|||
|
|
@ -6,28 +6,51 @@ Feature: search for users and hashtags
|
|||
|
||||
Background:
|
||||
Given following users exist:
|
||||
| username | email |
|
||||
| Bob Jones | bob@bob.bob |
|
||||
| Alice Smith | alice@alice.alice |
|
||||
And I sign in as "bob@bob.bob"
|
||||
| username | email |
|
||||
| Bob Jones | bob@bob.bob |
|
||||
| Alice Smith | alice@alice.alice |
|
||||
| Carol Williams | carol@example.com |
|
||||
|
||||
Scenario: search for a user and go to its profile
|
||||
When I enter "Alice Sm" in the search input
|
||||
Then I should see "Alice Smith" within ".ac_results"
|
||||
When I sign in as "bob@bob.bob"
|
||||
And I enter "Alice Sm" in the search input
|
||||
Then I should see "Alice Smith" within ".tt-menu"
|
||||
|
||||
When I click on the first search result
|
||||
Then I should see "Alice Smith" within ".profile_header #name"
|
||||
|
||||
Scenario: search for a inexistent user and go to the search page
|
||||
When I enter "Trinity" in the search input
|
||||
Then I should see "Search for Trinity" within ".ac_even"
|
||||
When I sign in as "bob@bob.bob"
|
||||
And I enter "Trinity" in the search input
|
||||
And I press enter in the search input
|
||||
|
||||
When I click on the first search result
|
||||
Then I should see "Users matching Trinity" within "#search_title"
|
||||
|
||||
Scenario: search for a not searchable user
|
||||
When I sign in as "carol@example.com"
|
||||
And I go to the edit profile page
|
||||
And I mark myself as not searchable
|
||||
And I submit the form
|
||||
Then I should be on the edit profile page
|
||||
And the "profile[searchable]" checkbox should not be checked
|
||||
|
||||
When I sign out
|
||||
And I sign in as "bob@bob.bob"
|
||||
And I enter "Carol Wi" in the search input
|
||||
Then I should not see any search results
|
||||
|
||||
Given a user with email "bob@bob.bob" is connected with "carol@example.com"
|
||||
When I go to the home page
|
||||
And I enter "Carol Wi" in the search input
|
||||
Then I should see "Carol Williams" within ".tt-menu"
|
||||
|
||||
When I click on the first search result
|
||||
Then I should see "Carol Williams" within ".profile_header #name"
|
||||
|
||||
Scenario: search for a tag
|
||||
When I enter "#Matrix" in the search input
|
||||
Then I should see "#matrix" within ".ac_even"
|
||||
When I sign in as "bob@bob.bob"
|
||||
And I enter "#Matrix" in the search input
|
||||
Then I should see "#Matrix" within ".tt-menu"
|
||||
|
||||
When I click on the first search result
|
||||
Then I should be on the tag page for "matrix"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ And /^I mark myself as safe for work$/ do
|
|||
uncheck('profile[nsfw]')
|
||||
end
|
||||
|
||||
And /^I mark myself as not searchable$/ do
|
||||
uncheck("profile[searchable]")
|
||||
end
|
||||
|
||||
When(/^I delete a photo$/) do
|
||||
find('.photo.loaded .thumbnail', :match => :first).hover
|
||||
find('.delete', :match => :first).click
|
||||
|
|
|
|||
|
|
@ -3,7 +3,15 @@ When /^I enter "([^"]*)" in the search input$/ do |search_term|
|
|||
end
|
||||
|
||||
When /^I click on the first search result$/ do
|
||||
within(".ac_results") do
|
||||
find("li", match: :first).click
|
||||
within(".tt-menu") do
|
||||
find(".tt-suggestion", match: :first).click
|
||||
end
|
||||
end
|
||||
|
||||
When /^I press enter in the search input$/ do
|
||||
find("input#q").native.send_keys :return
|
||||
end
|
||||
|
||||
Then /^I should not see any search results$/ do
|
||||
expect(page).to_not have_selector(".tt-suggestion")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -54,35 +54,4 @@ describe("app.views.Header", function() {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("search", function() {
|
||||
var input;
|
||||
|
||||
beforeEach(function() {
|
||||
$("#jasmine_content").html(this.view.el);
|
||||
input = $(this.view.el).find("#q");
|
||||
});
|
||||
|
||||
describe("focus", function() {
|
||||
beforeEach(function(done){
|
||||
input.trigger("focusin");
|
||||
done();
|
||||
});
|
||||
|
||||
it("adds the class 'active' when the user focuses the text field", function() {
|
||||
expect(input).toHaveClass("active");
|
||||
});
|
||||
});
|
||||
|
||||
describe("blur", function() {
|
||||
beforeEach(function(done) {
|
||||
input.trigger("focusin").trigger("focusout");
|
||||
done();
|
||||
});
|
||||
|
||||
it("removes the class 'active' when the user blurs the text field", function() {
|
||||
expect(input).not.toHaveClass("active");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,14 +1,78 @@
|
|||
describe("app.views.Search", function() {
|
||||
beforeEach(function(){
|
||||
spec.content().html('<form action="#" id="search_people_form"></form>');
|
||||
this.view = new app.views.Search({ el: '#search_people_form' });
|
||||
spec.content().html(
|
||||
"<form action='/search' id='search_people_form'><input id='q' name='q' type='search'></input></form>"
|
||||
);
|
||||
});
|
||||
describe("parse", function() {
|
||||
it("escapes a persons name", function() {
|
||||
var person = { 'name': '</script><script>alert("xss");</script' };
|
||||
this.view.context = this.view;
|
||||
var result = this.view.parse([$.extend({}, person)]);
|
||||
expect(result[0].data.name).not.toEqual(person.name);
|
||||
|
||||
describe("initialize", function() {
|
||||
it("calls setupBloodhound", function() {
|
||||
spyOn(app.views.Search.prototype, "setupBloodhound").and.callThrough();
|
||||
new app.views.Search({ el: "#search_people_form" });
|
||||
expect(app.views.Search.prototype.setupBloodhound).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls setupTypeahead", function() {
|
||||
spyOn(app.views.Search.prototype, "setupTypeahead");
|
||||
new app.views.Search({ el: "#search_people_form" });
|
||||
expect(app.views.Search.prototype.setupTypeahead).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("toggleSearchActive", function() {
|
||||
beforeEach(function() {
|
||||
this.view = new app.views.Search({ el: "#search_people_form" });
|
||||
this.typeaheadInput = this.view.$("#q");
|
||||
});
|
||||
|
||||
context("focus", function() {
|
||||
it("adds the class 'active' when the user focuses the text field", function() {
|
||||
expect(this.typeaheadInput).not.toHaveClass("active");
|
||||
this.typeaheadInput.trigger("focusin");
|
||||
expect(this.typeaheadInput).toHaveClass("active");
|
||||
});
|
||||
});
|
||||
|
||||
context("blur", function() {
|
||||
beforeEach(function() {
|
||||
this.typeaheadInput.addClass("active");
|
||||
});
|
||||
|
||||
it("removes the class 'active' when the user blurs the text field", function() {
|
||||
this.typeaheadInput.trigger("focusout");
|
||||
expect(this.typeaheadInput).not.toHaveClass("active");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("transformBloodhoundResponse" , function() {
|
||||
beforeEach(function() {
|
||||
this.view = new app.views.Search({ el: "#search_people_form" });
|
||||
});
|
||||
context("with persons", function() {
|
||||
beforeEach(function() {
|
||||
this.response = [{name: "Person", handle: "person@pod.tld"},{name: "User", handle: "user@pod.tld"}];
|
||||
});
|
||||
|
||||
it("sets data.person to true", function() {
|
||||
expect(this.view.transformBloodhoundResponse(this.response)).toEqual([
|
||||
{name: "Person", handle: "person@pod.tld", person: true},
|
||||
{name: "User", handle: "user@pod.tld", person: true}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
context("with hashtags", function() {
|
||||
beforeEach(function() {
|
||||
this.response = [{name: "#tag"}, {name: "#hashTag"}];
|
||||
});
|
||||
|
||||
it("sets data.hashtag to true and adds the correct URL", function() {
|
||||
expect(this.view.transformBloodhoundResponse(this.response)).toEqual([
|
||||
{name: "#tag", hashtag: true, url: Routes.tag("tag")},
|
||||
{name: "#hashTag", hashtag: true, url: Routes.tag("hashTag")}
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,763 +0,0 @@
|
|||
/*
|
||||
* Autocomplete - jQuery plugin 1.1pre
|
||||
*
|
||||
* Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
|
||||
*
|
||||
* Dual licensed under the MIT and GPL licenses:
|
||||
* http://www.opensource.org/licenses/mit-license.php
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*
|
||||
* Revision: $Id: jquery.autocomplete.js 5785 2008-07-12 10:37:33Z joern.zaefferer $
|
||||
* Modified by Diaspora
|
||||
*/
|
||||
|
||||
;(function($) {
|
||||
|
||||
$.fn.extend({
|
||||
autocomplete: function(urlOrData, options) {
|
||||
var isUrl = typeof urlOrData == "string";
|
||||
options = $.extend({}, $.Autocompleter.defaults, {
|
||||
url: isUrl ? urlOrData : null,
|
||||
data: isUrl ? null : urlOrData,
|
||||
delay: isUrl ? $.Autocompleter.defaults.delay : 10,
|
||||
max: options && !options.scroll ? 10 : 150
|
||||
}, options);
|
||||
|
||||
// if highlight is set to false, replace it with a do-nothing function
|
||||
options.highlight = options.highlight || function(value) { return value; };
|
||||
|
||||
// if the formatMatch option is not specified, then use formatItem for backwards compatibility
|
||||
options.formatMatch = options.formatMatch || options.formatItem;
|
||||
|
||||
return this.each(function() {
|
||||
new $.Autocompleter(this, options);
|
||||
});
|
||||
},
|
||||
result: function(handler) {
|
||||
return this.bind("result", handler);
|
||||
},
|
||||
search: function(handler) {
|
||||
return this.trigger("search", [handler]);
|
||||
},
|
||||
flushCache: function() {
|
||||
return this.trigger("flushCache");
|
||||
},
|
||||
setOptions: function(options){
|
||||
return this.trigger("setOptions", [options]);
|
||||
},
|
||||
unautocomplete: function() {
|
||||
return this.trigger("unautocomplete");
|
||||
}
|
||||
});
|
||||
|
||||
$.Autocompleter = function(input, options) {
|
||||
|
||||
var KEY = KEYCODES;
|
||||
|
||||
// Create $ object for input element
|
||||
var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
|
||||
|
||||
var timeout;
|
||||
var previousValue = "";
|
||||
var cache = $.Autocompleter.Cache(options);
|
||||
var hasFocus = 0;
|
||||
var lastKeyPressCode;
|
||||
var config = {
|
||||
mouseDownOnSelect: false
|
||||
};
|
||||
var select = $.Autocompleter.Select(options, input, selectCurrent, config);
|
||||
|
||||
var blockSubmit;
|
||||
|
||||
// prevent form submit in opera when selecting with return key
|
||||
$.browser.opera && $(input.form).bind("submit.autocomplete", function() {
|
||||
if (blockSubmit) {
|
||||
blockSubmit = false;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
|
||||
$input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) {
|
||||
// track last key pressed
|
||||
lastKeyPressCode = event.keyCode;
|
||||
switch(event.keyCode) {
|
||||
|
||||
case KEY.LEFT:
|
||||
case KEY.RIGHT:
|
||||
if( options.disableRightAndLeft && select.visible()){
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
case KEY.UP:
|
||||
if ( select.visible() ) {
|
||||
event.preventDefault();
|
||||
select.prev();
|
||||
} else {
|
||||
onChange(0, true);
|
||||
}
|
||||
break;
|
||||
|
||||
case KEY.DOWN:
|
||||
if ( select.visible() ) {
|
||||
event.preventDefault();
|
||||
select.next();
|
||||
} else {
|
||||
onChange(0, true);
|
||||
}
|
||||
break;
|
||||
|
||||
case KEY.PAGEUP:
|
||||
if ( select.visible() ) {
|
||||
event.preventDefault();
|
||||
select.pageUp();
|
||||
} else {
|
||||
onChange(0, true);
|
||||
}
|
||||
break;
|
||||
|
||||
case KEY.PAGEDOWN:
|
||||
if ( select.visible() ) {
|
||||
event.preventDefault();
|
||||
select.pageDown();
|
||||
} else {
|
||||
onChange(0, true);
|
||||
}
|
||||
break;
|
||||
|
||||
// matches also semicolon
|
||||
case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
|
||||
case KEY.TAB:
|
||||
case KEY.RETURN:
|
||||
if( selectCurrent() ) {
|
||||
// stop default to prevent a form submit, Opera needs special handling
|
||||
event.preventDefault();
|
||||
blockSubmit = true;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case KEY.ESC:
|
||||
select.hide();
|
||||
break;
|
||||
|
||||
default:
|
||||
options.onLetterTyped(event, $input);
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(onChange, options.delay);
|
||||
break;
|
||||
}
|
||||
}).focus(function(){
|
||||
// track whether the field has focus, we shouldn't process any
|
||||
// results if the field no longer has focus
|
||||
hasFocus++;
|
||||
}).blur(function() {
|
||||
hasFocus = 0;
|
||||
if (!config.mouseDownOnSelect) {
|
||||
hideResults();
|
||||
}
|
||||
}).click(function() {
|
||||
// show select when clicking in a focused field
|
||||
if ( hasFocus++ > 1 && !select.visible() ) {
|
||||
onChange(0, true);
|
||||
}
|
||||
}).bind("search", function() {
|
||||
// TODO why not just specifying both arguments?
|
||||
var fn = (arguments.length > 1) ? arguments[1] : null;
|
||||
function findValueCallback(q, data) {
|
||||
var result;
|
||||
if( data && data.length ) {
|
||||
for (var i=0; i < data.length; i++) {
|
||||
if( data[i].result.toLowerCase() == q.toLowerCase() ) {
|
||||
result = data[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if( typeof fn == "function" ) fn(result);
|
||||
else $input.trigger("result", result && [result.data, result.value]);
|
||||
}
|
||||
$.each(trimWords($input.val()), function(i, value) {
|
||||
request(value, findValueCallback, findValueCallback);
|
||||
});
|
||||
}).bind("flushCache", function() {
|
||||
cache.flush();
|
||||
}).bind("setOptions", function() {
|
||||
$.extend(options, arguments[1]);
|
||||
// if we've updated the data, repopulate
|
||||
if ( "data" in arguments[1] )
|
||||
cache.populate();
|
||||
}).bind("unautocomplete", function() {
|
||||
select.unbind();
|
||||
$input.unbind();
|
||||
$(input.form).unbind(".autocomplete");
|
||||
});
|
||||
|
||||
|
||||
function selectCurrent() {
|
||||
var selected = select.selected();
|
||||
if( !selected )
|
||||
return false;
|
||||
|
||||
var v = selected.result;
|
||||
previousValue = v;
|
||||
|
||||
if ( options.multiple ) {
|
||||
var words = trimWords($input.val());
|
||||
if ( words.length > 1 ) {
|
||||
v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v;
|
||||
}
|
||||
v += options.multipleSeparator;
|
||||
}
|
||||
|
||||
hideResultsNow();
|
||||
options.onSelect($input, selected.data, selected.value);
|
||||
return true;
|
||||
}
|
||||
|
||||
function onChange(crap, skipPrevCheck) {
|
||||
if( lastKeyPressCode == KEY.DEL ) {
|
||||
select.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
var currentValue = $input.val();
|
||||
|
||||
if ( !skipPrevCheck && currentValue == previousValue )
|
||||
return;
|
||||
|
||||
previousValue = currentValue;
|
||||
|
||||
currentValue = options.searchTermFromValue(currentValue, $input[0].selectionStart);
|
||||
if ( currentValue.length >= options.minChars) {
|
||||
$input.addClass(options.loadingClass);
|
||||
if (!options.matchCase)
|
||||
currentValue = currentValue.toLowerCase();
|
||||
request(currentValue, receiveData, hideResultsNow);
|
||||
} else {
|
||||
stopLoading();
|
||||
select.hide();
|
||||
}
|
||||
};
|
||||
|
||||
function trimWords(value) {
|
||||
if ( !value ) {
|
||||
return [""];
|
||||
}
|
||||
var words = value.split( options.multipleSeparator );
|
||||
var result = [];
|
||||
$.each(words, function(i, value) {
|
||||
if ( $.trim(value) )
|
||||
result[i] = $.trim(value);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// fills in the input box w/the first match (assumed to be the best match)
|
||||
// q: the term entered
|
||||
// sValue: the first matching result
|
||||
function autoFill(q, sValue){
|
||||
// autofill in the complete box w/the first match as long as the user hasn't entered in more data
|
||||
// if the last user key pressed was backspace, don't autofill
|
||||
if( options.autoFill && (options.lastWord($input.val(), null, options.multiple).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) {
|
||||
// fill in the value (keep the case the user has typed)
|
||||
$input.val($input.val() + sValue.substring(options.lastWord(previousValue, null, options.multiple).length));
|
||||
// select the portion of the value not typed by the user (so the next character will erase)
|
||||
$.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length);
|
||||
}
|
||||
};
|
||||
|
||||
function hideResults() {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(hideResultsNow, 200);
|
||||
};
|
||||
|
||||
function hideResultsNow() {
|
||||
select.hide();
|
||||
clearTimeout(timeout);
|
||||
stopLoading();
|
||||
if (options.mustMatch) {
|
||||
// call search and run callback
|
||||
$input.search(
|
||||
function (result){
|
||||
// if no value found, clear the input box
|
||||
if( !result ) {
|
||||
if (options.multiple) {
|
||||
var words = trimWords($input.val()).slice(0, -1);
|
||||
$input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
|
||||
}
|
||||
else
|
||||
$input.val( "" );
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function receiveData(q, data) {
|
||||
if ( data && data.length && hasFocus ) {
|
||||
stopLoading();
|
||||
select.display(data, q);
|
||||
autoFill(q, data[0].value);
|
||||
select.show();
|
||||
} else {
|
||||
hideResultsNow();
|
||||
}
|
||||
};
|
||||
|
||||
function request(term, success, failure) {
|
||||
if (!options.matchCase)
|
||||
term = term.toLowerCase();
|
||||
var data = cache.load(term);
|
||||
// recieve the cached data
|
||||
if (data && data.length) {
|
||||
success(term, data);
|
||||
// if an AJAX url has been supplied, try loading the data now
|
||||
} else if( (typeof options.url == "string") && (options.url.length > 0) ){
|
||||
|
||||
var extraParams = {
|
||||
timestamp: +new Date()
|
||||
};
|
||||
$.each(options.extraParams, function(key, param) {
|
||||
extraParams[key] = typeof param == "function" ? param() : param;
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
// try to leverage ajaxQueue plugin to abort previous requests
|
||||
mode: "abort",
|
||||
// limit abortion to this input
|
||||
port: "autocomplete" + input.name,
|
||||
dataType: options.dataType,
|
||||
url: options.url,
|
||||
data: $.extend({
|
||||
q: options.lastWord(term, null, options.multiple),
|
||||
limit: options.max
|
||||
}, extraParams),
|
||||
success: function(data) {
|
||||
var parsed = options.parse && options.parse(data) || parse(data);
|
||||
cache.add(term, parsed);
|
||||
success(term, parsed);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
|
||||
select.emptyList();
|
||||
failure(term);
|
||||
}
|
||||
};
|
||||
|
||||
function parse(data) {
|
||||
var parsed = [];
|
||||
var rows = data.split("\n");
|
||||
for (var i=0; i < rows.length; i++) {
|
||||
var row = $.trim(rows[i]);
|
||||
if (row) {
|
||||
row = row.split("|");
|
||||
parsed[parsed.length] = {
|
||||
data: row,
|
||||
value: row[0],
|
||||
result: options.formatResult && options.formatResult(row, row[0]) || row[0]
|
||||
};
|
||||
}
|
||||
}
|
||||
return parsed;
|
||||
};
|
||||
|
||||
function stopLoading() {
|
||||
$input.removeClass(options.loadingClass);
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
$.Autocompleter.defaults = {
|
||||
onLetterTyped : function(event){},
|
||||
lastWord : function(value, crap, multiple) {
|
||||
if ( !multiple )
|
||||
return value;
|
||||
var words = trimWords(value);
|
||||
return words[words.length - 1];
|
||||
},
|
||||
inputClass: "ac_input",
|
||||
resultsClass: "ac_results",
|
||||
loadingClass: "ac_loading",
|
||||
onSelect: function(input, data, formatted){
|
||||
if (select.visible())
|
||||
// position cursor at end of input field
|
||||
$.Autocompleter.Selection(input, input.value.length, input.value.length);
|
||||
input.val(formatted);
|
||||
},
|
||||
minChars: 1,
|
||||
delay: 400,
|
||||
matchCase: false,
|
||||
matchSubset: true,
|
||||
matchContains: false,
|
||||
cacheLength: 10,
|
||||
max: 100,
|
||||
mustMatch: false,
|
||||
extraParams: {},
|
||||
selectFirst: true,
|
||||
formatItem: function(row) { return row[0]; },
|
||||
selectionChanged : function(newItem) {},
|
||||
formatMatch: null,
|
||||
autoFill: false,
|
||||
width: 0,
|
||||
multiple: false,
|
||||
multipleSeparator: ", ",
|
||||
disableRightAndLeft: false,
|
||||
highlight: function(value, term) {
|
||||
return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
|
||||
},
|
||||
scroll: true,
|
||||
scrollHeight: 180
|
||||
};
|
||||
$.Autocompleter.defaults.searchTermFromValue = $.Autocompleter.defaults.lastWord;
|
||||
|
||||
$.Autocompleter.Cache = function(options) {
|
||||
|
||||
var data = {};
|
||||
var length = 0;
|
||||
|
||||
function matchSubset(s, sub) {
|
||||
if (!options.matchCase)
|
||||
s = s.toLowerCase();
|
||||
var i = s.indexOf(sub);
|
||||
if (options.matchContains == "word"){
|
||||
i = s.toLowerCase().search("\\b" + sub.toLowerCase());
|
||||
}
|
||||
if (i == -1) return false;
|
||||
return i == 0 || options.matchContains;
|
||||
};
|
||||
|
||||
function add(q, value) {
|
||||
if (length > options.cacheLength){
|
||||
flush();
|
||||
}
|
||||
if (!data[q]){
|
||||
length++;
|
||||
}
|
||||
data[q] = value;
|
||||
}
|
||||
|
||||
function populate(){
|
||||
if( !options.data ) return false;
|
||||
// track the matches
|
||||
var stMatchSets = {},
|
||||
nullData = 0;
|
||||
|
||||
// no url was specified, we need to adjust the cache length to make sure it fits the local data store
|
||||
if( !options.url ) options.cacheLength = 1;
|
||||
|
||||
// track all options for minChars = 0
|
||||
stMatchSets[""] = [];
|
||||
|
||||
// loop through the array and create a lookup structure
|
||||
for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
|
||||
var rawValue = options.data[i];
|
||||
// if rawValue is a string, make an array otherwise just reference the array
|
||||
rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
|
||||
|
||||
var value = options.formatMatch(rawValue, i+1, options.data.length);
|
||||
if ( value === false )
|
||||
continue;
|
||||
|
||||
var firstChar = value.charAt(0).toLowerCase();
|
||||
// if no lookup array for this character exists, look it up now
|
||||
if( !stMatchSets[firstChar] )
|
||||
stMatchSets[firstChar] = [];
|
||||
|
||||
// if the match is a string
|
||||
var row = {
|
||||
value: value,
|
||||
data: rawValue,
|
||||
result: options.formatResult && options.formatResult(rawValue) || value
|
||||
};
|
||||
|
||||
// push the current match into the set list
|
||||
stMatchSets[firstChar].push(row);
|
||||
|
||||
// keep track of minChars zero items
|
||||
if ( nullData++ < options.max ) {
|
||||
stMatchSets[""].push(row);
|
||||
}
|
||||
};
|
||||
|
||||
// add the data items to the cache
|
||||
$.each(stMatchSets, function(i, value) {
|
||||
// increase the cache size
|
||||
options.cacheLength++;
|
||||
// add to the cache
|
||||
add(i, value);
|
||||
});
|
||||
}
|
||||
|
||||
// populate any existing data
|
||||
setTimeout(populate, 25);
|
||||
|
||||
function flush(){
|
||||
data = {};
|
||||
length = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
flush: flush,
|
||||
add: add,
|
||||
populate: populate,
|
||||
load: function(q) {
|
||||
if (!options.cacheLength || !length)
|
||||
return null;
|
||||
/*
|
||||
* if dealing w/local data and matchContains than we must make sure
|
||||
* to loop through all the data collections looking for matches
|
||||
*/
|
||||
if( !options.url && options.matchContains ){
|
||||
// track all matches
|
||||
var csub = [];
|
||||
// loop through all the data grids for matches
|
||||
for( var k in data ){
|
||||
// don't search through the stMatchSets[""] (minChars: 0) cache
|
||||
// this prevents duplicates
|
||||
if( k.length > 0 ){
|
||||
var c = data[k];
|
||||
$.each(c, function(i, x) {
|
||||
// if we've got a match, add it to the array
|
||||
if (matchSubset(x.value, q)) {
|
||||
csub.push(x);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return csub;
|
||||
} else
|
||||
// if the exact item exists, use it
|
||||
if (data[q]){
|
||||
return data[q];
|
||||
} else
|
||||
if (options.matchSubset) {
|
||||
for (var i = q.length - 1; i >= options.minChars; i--) {
|
||||
var c = data[q.substr(0, i)];
|
||||
if (c) {
|
||||
var csub = [];
|
||||
$.each(c, function(i, x) {
|
||||
if (matchSubset(x.value, q)) {
|
||||
csub[csub.length] = x;
|
||||
}
|
||||
});
|
||||
return csub;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
$.Autocompleter.Select = function (options, input, select, config) {
|
||||
var CLASSES = {
|
||||
ACTIVE: "ac_over"
|
||||
};
|
||||
|
||||
var listItems,
|
||||
active = -1,
|
||||
data,
|
||||
term = "",
|
||||
needsInit = true,
|
||||
element,
|
||||
list;
|
||||
|
||||
// Create results
|
||||
function init() {
|
||||
if (!needsInit)
|
||||
return;
|
||||
element = $("<div/>")
|
||||
.hide()
|
||||
.addClass(options.resultsClass)
|
||||
.css("position", "fixed")
|
||||
.appendTo(document.body);
|
||||
|
||||
list = $("<ul/>").appendTo(element).mouseover( function(event) {
|
||||
if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
|
||||
active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
|
||||
$(target(event)).addClass(CLASSES.ACTIVE);
|
||||
}
|
||||
}).click(function(event) {
|
||||
$(target(event)).addClass(CLASSES.ACTIVE);
|
||||
select();
|
||||
// TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus
|
||||
input.focus();
|
||||
return false;
|
||||
}).mousedown(function() {
|
||||
config.mouseDownOnSelect = true;
|
||||
}).mouseup(function() {
|
||||
config.mouseDownOnSelect = false;
|
||||
});
|
||||
|
||||
if( options.width > 0 )
|
||||
element.css("width", options.width);
|
||||
|
||||
needsInit = false;
|
||||
}
|
||||
|
||||
function target(event) {
|
||||
var element = event.target;
|
||||
while(element && element.tagName != "LI")
|
||||
element = element.parentNode;
|
||||
// more fun with IE, sometimes event.target is empty, just ignore it then
|
||||
if(!element)
|
||||
return [];
|
||||
return element;
|
||||
}
|
||||
|
||||
function moveSelect(step) {
|
||||
listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE);
|
||||
movePosition(step);
|
||||
var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
|
||||
if(options.scroll) {
|
||||
var offset = 0;
|
||||
listItems.slice(0, active).each(function() {
|
||||
offset += this.offsetHeight;
|
||||
});
|
||||
if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
|
||||
list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
|
||||
} else if(offset < list.scrollTop()) {
|
||||
list.scrollTop(offset);
|
||||
}
|
||||
}
|
||||
options.selectionChanged(activeItem);
|
||||
};
|
||||
|
||||
function movePosition(step) {
|
||||
active += step;
|
||||
if (active < 0) {
|
||||
active = listItems.size() - 1;
|
||||
} else if (active >= listItems.size()) {
|
||||
active = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function limitNumberOfItems(available) {
|
||||
return options.max && options.max < available
|
||||
? options.max
|
||||
: available;
|
||||
}
|
||||
|
||||
function fillList() {
|
||||
list.empty();
|
||||
var max = limitNumberOfItems(data.length);
|
||||
for (var i=0; i < max; i++) {
|
||||
if (!data[i])
|
||||
continue;
|
||||
var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
|
||||
if ( formatted === false )
|
||||
continue;
|
||||
var li = $("<li/>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0];
|
||||
$.data(li, "ac_data", data[i]);
|
||||
}
|
||||
listItems = list.find("li");
|
||||
if ( options.selectFirst ) {
|
||||
listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
|
||||
active = 0;
|
||||
}
|
||||
// apply bgiframe if available
|
||||
if ( $.fn.bgiframe )
|
||||
list.bgiframe();
|
||||
}
|
||||
|
||||
return {
|
||||
display: function(d, q) {
|
||||
init();
|
||||
data = d;
|
||||
term = q;
|
||||
fillList();
|
||||
},
|
||||
next: function() {
|
||||
moveSelect(1);
|
||||
},
|
||||
prev: function() {
|
||||
moveSelect(-1);
|
||||
},
|
||||
pageUp: function() {
|
||||
if (active != 0 && active - 8 < 0) {
|
||||
moveSelect( -active );
|
||||
} else {
|
||||
moveSelect(-8);
|
||||
}
|
||||
},
|
||||
pageDown: function() {
|
||||
if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
|
||||
moveSelect( listItems.size() - 1 - active );
|
||||
} else {
|
||||
moveSelect(8);
|
||||
}
|
||||
},
|
||||
hide: function() {
|
||||
element && element.hide();
|
||||
listItems && listItems.removeClass(CLASSES.ACTIVE);
|
||||
active = -1;
|
||||
},
|
||||
visible : function() {
|
||||
return element && element.is(":visible");
|
||||
},
|
||||
current: function() {
|
||||
return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
|
||||
},
|
||||
show: function() {
|
||||
var offset = $(input).offset();
|
||||
element.css({
|
||||
width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
|
||||
top: offset.top + input.offsetHeight - $("nav.navbar").offset().top,
|
||||
left: offset.left
|
||||
}).show();
|
||||
if(options.scroll) {
|
||||
list.scrollTop(0);
|
||||
list.css({
|
||||
maxHeight: options.scrollHeight,
|
||||
overflow: 'auto'
|
||||
});
|
||||
|
||||
if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
|
||||
var listHeight = 0;
|
||||
listItems.each(function() {
|
||||
listHeight += this.offsetHeight;
|
||||
});
|
||||
var scrollbarsVisible = listHeight > options.scrollHeight;
|
||||
list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
|
||||
if (!scrollbarsVisible) {
|
||||
// IE doesn't recalculate width when scrollbar disappears
|
||||
listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
selected: function() {
|
||||
var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
|
||||
return selected && selected.length && $.data(selected[0], "ac_data");
|
||||
},
|
||||
emptyList: function (){
|
||||
list && list.empty();
|
||||
},
|
||||
unbind: function() {
|
||||
element && element.remove();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
$.Autocompleter.Selection = function(field, start, end) {
|
||||
if( field.createTextRange ){
|
||||
var selRange = field.createTextRange();
|
||||
selRange.collapse(true);
|
||||
selRange.moveStart("character", start);
|
||||
selRange.moveEnd("character", end);
|
||||
selRange.select();
|
||||
} else if( field.setSelectionRange ){
|
||||
field.setSelectionRange(start, end);
|
||||
} else {
|
||||
if( field.selectionStart ){
|
||||
field.selectionStart = start;
|
||||
field.selectionEnd = end;
|
||||
}
|
||||
}
|
||||
field.focus();
|
||||
};
|
||||
|
||||
})(jQuery);
|
||||
Loading…
Reference in a new issue