diff --git a/Gemfile b/Gemfile
index 9ccad44a7..2683adc4f 100644
--- a/Gemfile
+++ b/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
diff --git a/Gemfile.lock b/Gemfile.lock
index cf28ab54b..108d579e4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -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)
diff --git a/app/assets/javascripts/app/views/header_view.js b/app/assets/javascripts/app/views/header_view.js
index 6434f1a89..5b682c3b3 100644
--- a/app/assets/javascripts/app/views/header_view.js
+++ b/app/assets/javascripts/app/views/header_view.js
@@ -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
diff --git a/app/assets/javascripts/app/views/search_view.js b/app/assets/javascripts/app/views/search_view.js
index aed2e54ca..6e8a1d272 100644
--- a/app/assets/javascripts/app/views/search_view.js
+++ b/app/assets/javascripts/app/views/search_view.js
@@ -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 += '
'; }
- item += row.name;
- if (row.handle) { item += '
' + row.handle + '
'; }
- 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();
}
}
});
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 8ee4dfc2b..cbad4371f 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -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
diff --git a/app/assets/javascripts/view.js b/app/assets/javascripts/view.js
index 5f43ad4dd..4810a8a69 100644
--- a/app/assets/javascripts/view.js
+++ b/app/assets/javascripts/view.js
@@ -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");
diff --git a/app/assets/stylesheets/_application.scss b/app/assets/stylesheets/_application.scss
index a0b6d7d0c..585929f2b 100644
--- a/app/assets/stylesheets/_application.scss
+++ b/app/assets/stylesheets/_application.scss
@@ -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';
diff --git a/app/assets/stylesheets/autocomplete.scss b/app/assets/stylesheets/autocomplete.scss
deleted file mode 100644
index 051dd6580..000000000
--- a/app/assets/stylesheets/autocomplete.scss
+++ /dev/null
@@ -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;
- }
-}
diff --git a/app/assets/stylesheets/typeahead.scss b/app/assets/stylesheets/typeahead.scss
new file mode 100644
index 000000000..4c08bc219
--- /dev/null
+++ b/app/assets/stylesheets/typeahead.scss
@@ -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; }
+ }
+}
diff --git a/app/assets/templates/header_tpl.jst.hbs b/app/assets/templates/header_tpl.jst.hbs
index f00f23729..e835337a1 100644
--- a/app/assets/templates/header_tpl.jst.hbs
+++ b/app/assets/templates/header_tpl.jst.hbs
@@ -101,8 +101,7 @@
diff --git a/app/assets/templates/search_suggestion_tpl.jst.hbs b/app/assets/templates/search_suggestion_tpl.jst.hbs
new file mode 100644
index 000000000..a2c734cab
--- /dev/null
+++ b/app/assets/templates/search_suggestion_tpl.jst.hbs
@@ -0,0 +1,13 @@
+{{#if person}}
+
+ {{#if avatar}}
+

+ {{/if}}
+
{{ name }}
+
{{ handle }}
+
+{{else}}{{#if hashtag}}
+
+{{/if}}{{/if}}
diff --git a/config/.jshint.json b/config/.jshint.json
index 86bcc1cf2..676c0f1f6 100644
--- a/config/.jshint.json
+++ b/config/.jshint.json
@@ -40,6 +40,7 @@
"_",
"autosize",
"Backbone",
+ "Bloodhound",
"gon",
"Handlebars",
"HandlebarsTemplates",
diff --git a/features/desktop/search.feature b/features/desktop/search.feature
index da49164df..8b436018f 100644
--- a/features/desktop/search.feature
+++ b/features/desktop/search.feature
@@ -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"
diff --git a/features/step_definitions/profile_steps.rb b/features/step_definitions/profile_steps.rb
index 28a5a3489..af9949d93 100644
--- a/features/step_definitions/profile_steps.rb
+++ b/features/step_definitions/profile_steps.rb
@@ -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
diff --git a/features/step_definitions/search_steps.rb b/features/step_definitions/search_steps.rb
index c623c202d..18b499989 100644
--- a/features/step_definitions/search_steps.rb
+++ b/features/step_definitions/search_steps.rb
@@ -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
diff --git a/spec/javascripts/app/views/header_view_spec.js b/spec/javascripts/app/views/header_view_spec.js
index 1106d6d6f..2b1987ff8 100644
--- a/spec/javascripts/app/views/header_view_spec.js
+++ b/spec/javascripts/app/views/header_view_spec.js
@@ -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");
- });
- });
- });
});
diff --git a/spec/javascripts/app/views/search_view_spec.js b/spec/javascripts/app/views/search_view_spec.js
index ca23e6f7c..19fc0ccfb 100644
--- a/spec/javascripts/app/views/search_view_spec.js
+++ b/spec/javascripts/app/views/search_view_spec.js
@@ -1,14 +1,78 @@
describe("app.views.Search", function() {
beforeEach(function(){
- spec.content().html('');
- this.view = new app.views.Search({ el: '#search_people_form' });
+ spec.content().html(
+ ""
+ );
});
- describe("parse", function() {
- it("escapes a persons name", function() {
- var person = { 'name': ' 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"), "$1");
- },
- 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 = $("")
- .hide()
- .addClass(options.resultsClass)
- .css("position", "fixed")
- .appendTo(document.body);
-
- list = $("").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 = $("").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);