diff --git a/app/controllers/people_controller.rb b/app/controllers/people_controller.rb
index 824ee9487..b7f020606 100644
--- a/app/controllers/people_controller.rb
+++ b/app/controllers/people_controller.rb
@@ -10,14 +10,15 @@ class PeopleController < ApplicationController
def index
@aspect = :search
+ params[:q] ||= params[:term]
@people = Person.search(params[:q], current_user).paginate :page => params[:page], :per_page => 15
@hashes = hashes_for_people(@people, @aspects)
- @people
#only do it if it is an email address
if params[:q].try(:match, Devise.email_regexp)
webfinger(params[:q])
end
+ respond_with @people
end
def hashes_for_people people, aspects
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 3ca7167af..9b1a47680 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -135,7 +135,7 @@ module ApplicationHelper
end
def person_image_tag(person, size=:thumb_small)
- "".html_safe
+ "
".html_safe
end
def person_link(person)
@@ -144,13 +144,6 @@ module ApplicationHelper
".html_safe
end
- def image_or_default(person, size=:thumb_large)
- image_location = person.profile.image_url(size) if person.profile
- image_location ||= person.profile.image_url(:thumb_large) if person.profile #backwards compatability for old profile pictures
- image_location ||= "/images/user/default.png"
- image_location
- end
-
def hard_link(string, path)
link_to string, path, :rel => 'external'
end
@@ -277,7 +270,7 @@ module ApplicationHelper
image_tag 'icons/monotone_question.png', :class => 'what_is_this', :title => text
end
-
+
def get_javascript_strings_for(language)
I18n.t('javascripts')
end
diff --git a/app/models/person.rb b/app/models/person.rb
index 66b5ca459..82f0c81d9 100644
--- a/app/models/person.rb
+++ b/app/models/person.rb
@@ -160,15 +160,7 @@ class Person < ActiveRecord::Base
end
def as_json(opts={})
- {
- :person => {
- :id => self.guid,
- :name => self.name,
- :url => self.url,
- :exported_key => exported_key,
- :diaspora_handle => self.diaspora_handle
- }
- }
+ {:id => self.guid, :name => self.name, :avatar => self.profile.image_url(:thumb_small), :url => "/people/#{self.id}"}
end
protected
diff --git a/app/models/profile.rb b/app/models/profile.rb
index 47d709e84..ea5e7c42a 100644
--- a/app/models/profile.rb
+++ b/app/models/profile.rb
@@ -45,13 +45,14 @@ class Profile < ActiveRecord::Base
end
def image_url(size = :thumb_large)
- if size == :thumb_medium
- self[:image_url_medium]
- elsif size == :thumb_small
- self[:image_url_small]
- else
- self[:image_url]
- end
+ result = if size == :thumb_medium && self[:image_url_medium]
+ self[:image_url_medium]
+ elsif size == :thumb_small && self[:image_url_small]
+ self[:image_url_small]
+ else
+ self[:image_url]
+ end
+ result || '/images/user/default.png'
end
def image_url= url
diff --git a/app/views/publics/hcard.haml b/app/views/publics/hcard.haml
index f6e4c4408..ca0194e3d 100644
--- a/app/views/publics/hcard.haml
+++ b/app/views/publics/hcard.haml
@@ -32,18 +32,18 @@
%dl.entity_photo
%dt Photo
%dd
- %img.photo.avatar{:src=>image_or_default(@person), :width=>'300px', :height=>'300px'}
+ %img.photo.avatar{:src=>@person.profile.image_url, :width=>'300px', :height=>'300px'}
%dl.entity_photo_medium
%dt Photo
%dd
- %img.photo.avatar{:src=>image_or_default(@person, :thumb_medium), :width=>'100px', :height=>'100px'}
-
+ %img.photo.avatar{:src=>@person.profile.image_url(:thumb_medium), :width=>'100px', :height=>'100px'}
+
%dl.entity_photo_small
%dt Photo
%dd
- %img.photo.avatar{:src=>image_or_default(@person, :thumb_small), :width=>'50px', :height=>'50px'}
-
+ %img.photo.avatar{:src=>@person.profile.image_url(:thumb_small), :width=>'50px', :height=>'50px'}
+
%dl.entity_searchable
%dt Searchable
%dd
diff --git a/app/views/publics/hcard.html.haml b/app/views/publics/hcard.html.haml
deleted file mode 100644
index f6e4c4408..000000000
--- a/app/views/publics/hcard.html.haml
+++ /dev/null
@@ -1,50 +0,0 @@
-#content
- %h1= @person.name
- #content_inner
- #i.entity_profile.vcard.author
- %h2 User profile
-
- %dl.entity_nickname
- %dt Nickname
- %dd
- %a.nickname.url.uid{:href=>@person.url, :rel=>'me'}= @person.name
-
- %dl.entity_given_name
- %dt First name
- %dd
- %span.given_name= @person.profile.first_name
-
- %dl.entity_family_name
- %dt Family name
- %dd
- %span.family_name= @person.profile.last_name
-
- %dl.entity_fn
- %dt Full name
- %dd
- %span.fn= @person.name
-
- %dl.entity_url
- %dt URL
- %dd
- %a#pod_location.url{:href=>@person.url, :rel=>'me'}= @person.url
-
- %dl.entity_photo
- %dt Photo
- %dd
- %img.photo.avatar{:src=>image_or_default(@person), :width=>'300px', :height=>'300px'}
-
- %dl.entity_photo_medium
- %dt Photo
- %dd
- %img.photo.avatar{:src=>image_or_default(@person, :thumb_medium), :width=>'100px', :height=>'100px'}
-
- %dl.entity_photo_small
- %dt Photo
- %dd
- %img.photo.avatar{:src=>image_or_default(@person, :thumb_small), :width=>'50px', :height=>'50px'}
-
- %dl.entity_searchable
- %dt Searchable
- %dd
- %span.searchable= @person.profile.searchable
diff --git a/config/assets.yml b/config/assets.yml
index 184cf2e8f..fd07d77ef 100644
--- a/config/assets.yml
+++ b/config/assets.yml
@@ -10,11 +10,12 @@ javascripts:
- public/javascripts/vendor/jquery.tipsy.js
- public/javascripts/vendor/jquery.hotkeys.js
- public/javascripts/vendor/jquery.autoresize.min.js
- - public/javascripts/vendor/facebox.js
- public/javascripts/vendor/jquery.infinitescroll.min.js
+ - public/javascripts/vendor/facebox.js
- public/javascripts/vendor/timeago.js
- public/javascripts/vendor/fileuploader.js
- public/javascripts/vendor/Mustache.js
+ - public/javascripts/jquery.autocomplete-custom.js
- public/javascripts/diaspora.js
- public/javascripts/widgets/i18n.js
- public/javascripts/widgets/alert.js
@@ -23,10 +24,11 @@ javascripts:
- public/javascripts/view.js
- public/javascripts/stream.js
- public/javascripts/application.js
+ - public/javascripts/search.js
mobile:
- public/javascripts/vendor/jquery144.min.js
- - public/javascripts/custom-mobile-scripting.js
- public/javascripts/vendor/jquery-ui-1.8.6.custom.min.js
+ - public/javascripts/custom-mobile-scripting.js
- public/javascripts/vendor/jquery_mobile_a2.min.js
- public/javascripts/vendor/jquery.infinitescroll.min.js
- public/javascripts/vendor/timeago.js
@@ -41,7 +43,6 @@ javascripts:
- public/javascripts/vendor/mailchimp/jquery.validate.js
- public/javascripts/vendor/mailchimp/jquery126.min.js
aspects:
- - public/javascripts/vendor/jquery-ui-1.8.6.custom.min.js
- public/javascripts/aspect-edit.js
- public/javascripts/contact-list.js
home:
@@ -57,7 +58,8 @@ stylesheets:
default:
- public/stylesheets/application.css
- public/stylesheets/ui.css
+ - public/stylesheets/autocomplete.css
- public/stylesheets/vendor/facebox.css
- public/stylesheets/vendor/fileuploader.css
- public/stylesheets/vendor/tipsy.css
-
\ No newline at end of file
+
diff --git a/public/javascripts/application.js b/public/javascripts/application.js
index 0c53a7bd6..e48935afb 100644
--- a/public/javascripts/application.js
+++ b/public/javascripts/application.js
@@ -20,15 +20,18 @@ $(document).ready(function() {
});
$(window).unbind('.infscr');
-
+
$("a.paginate").live("click", function() {
$(this).css("display", "none");
-
+
$(document).trigger("retrieve.infscr");
});
$('a').live('tap',function(){
$(this).addClass('tapped');
})
+
+ // autocomplete search box
+ Search.initialize();
});
diff --git a/public/javascripts/jquery.autocomplete-custom.js b/public/javascripts/jquery.autocomplete-custom.js
new file mode 100644
index 000000000..80d7779d5
--- /dev/null
+++ b/public/javascripts/jquery.autocomplete-custom.js
@@ -0,0 +1,764 @@
+/*
+ * 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 = {
+ UP: 38,
+ DOWN: 40,
+ DEL: 46,
+ TAB: 9,
+ RETURN: 13,
+ ESC: 27,
+ COMMA: 188,
+ PAGEUP: 33,
+ PAGEDOWN: 34,
+ BACKSPACE: 8
+ };
+
+ // 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.UP:
+ event.preventDefault();
+ if ( select.visible() ) {
+ select.prev();
+ } else {
+ onChange(0, true);
+ }
+ break;
+
+ case KEY.DOWN:
+ event.preventDefault();
+ if ( select.visible() ) {
+ select.next();
+ } else {
+ onChange(0, true);
+ }
+ break;
+
+ case KEY.PAGEUP:
+ event.preventDefault();
+ if ( select.visible() ) {
+ select.pageUp();
+ } else {
+ onChange(0, true);
+ }
+ break;
+
+ case KEY.PAGEDOWN:
+ event.preventDefault();
+ if ( select.visible() ) {
+ 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:
+ 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;
+ }
+
+ $input.val(v);
+ hideResultsNow();
+ $input.trigger("result", [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 = lastWord(currentValue);
+ 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;
+ }
+
+ function lastWord(value) {
+ if ( !options.multiple )
+ return value;
+ var words = trimWords(value);
+ return words[words.length - 1];
+ }
+
+ // 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 && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) {
+ // fill in the value (keep the case the user has typed)
+ $input.val($input.val() + sValue.substring(lastWord(previousValue).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() {
+ var wasVisible = select.visible();
+ 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( "" );
+ }
+ }
+ );
+ }
+ if (wasVisible)
+ // position cursor at end of input field
+ $.Autocompleter.Selection(input, input.value.length, input.value.length);
+ };
+
+ 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: lastWord(term),
+ 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 = {
+ inputClass: "ac_input",
+ resultsClass: "ac_results",
+ loadingClass: "ac_loading",
+ 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: ", ",
+ highlight: function(value, term) {
+ return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1");
+ },
+ scroll: true,
+ scrollHeight: 180
+};
+
+$.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 = $("