/*
* Copyright 2012, Google Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @author Dae Park (daepark@google.com)
*/
(function($, undefined){
if (!("console" in window)) {
var c = window.console = {};
c.log = c.warn = c.error = c.debug = function(){};
}
/**
* jQuery UI provides a way to be notified when an element is removed from the DOM.
* suggest would like to use this facility to properly teardown it's elements from the DOM (suggest list, flyout, etc.).
* The following logic tries to determine if "remove" event is already present, else
* tries to mimic what jQuery UI does (as of 1.8.5) by adding a hook to $.cleanData or $.fn.remove.
*/
$(function() {
var div = $("
");
$(document.body).append(div);
var t = setTimeout(function() {
// copied from jquery-ui
// for remove event
if ( $.cleanData ) {
var _cleanData = $.cleanData;
$.cleanData = function( elems ) {
for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
$( elem ).triggerHandler( "remove" );
}
_cleanData( elems );
};
}
else {
var _remove = $.fn.remove;
$.fn.remove = function( selector, keepData ) {
return this.each(function() {
if ( !keepData ) {
if ( !selector || $.filter( selector, [ this ] ).length ) {
$( "*", this ).add( [ this ] ).each(function() {
$( this ).triggerHandler( "remove" );
});
}
}
return _remove.call( $(this), selector, keepData );
});
};
}
}, 1);
div.bind("remove", function() {
clearTimeout(t);
});
div.remove();
});
/**
* These are the search parameters that are transparently passed
* to the search service as specified by service_url + service_path
*/
var SEARCH_PARAMS = {
key:1, filter:1, spell:1, exact:1,
lang:1, scoring:1, prefixed:1, stemmed:1, format:1, mql_output:1,
output:1, type:1
};
$.suggest = function(name, prototype) {
$.fn[name] = function(options) {
if (!this.length) {
console.warn('Suggest: invoked on empty element set');
}
return this
.each(function() {
if (this.nodeName) {
if (this.nodeName.toUpperCase() === 'INPUT') {
if (this.type && this.type.toUpperCase() !== 'TEXT') {
console.warn('Suggest: unsupported INPUT type: '+this.type);
}
}
else {
console.warn('Suggest: unsupported DOM element: '+this.nodeName);
}
}
var instance = $.data(this, name);
if (instance) {
instance._destroy();
}
$.data(this, name, new $.suggest[name](this, options))._init();
});
};
$.suggest[name] = function(input, options) {
var self = this,
o = this.options = $.extend(true, {},
$.suggest.defaults,
$.suggest[name].defaults,
options),
pfx = o.css_prefix = o.css_prefix || "",
css = o.css;
this.name = name;
$.each(css, function(k, v) {
css[k] = pfx + css[k];
});
// suggest parameters
o.ac_param = {};
$.each(SEARCH_PARAMS, function(k) {
var v = o[k];
if (v === null || v === "") {
return;
}
o.ac_param[k] = v;
});
// flyout service lang is the first specified lang
o.flyout_lang = null;
if (o.ac_param.lang) {
var lang = o.ac_param.lang;
if ($.isArray(lang) && lang.length) {
lang = lang.join(',');
}
if (lang) {
o.flyout_lang = lang;
}
}
// status texts
this._status = {
START: "",
LOADING: "",
SELECT: "",
ERROR: ""
};
if (o.status && o.status instanceof Array && o.status.length >= 3) {
this._status.START = o.status[0] || "";
this._status.LOADING = o.status[1] || "";
this._status.SELECT = o.status[2] || "";
if (o.status.length === 4) {
this._status.ERROR = o.status[3] || "";
}
}
// create the container for the drop down list
var s = this.status = $('
').addClass(css.status),
l = this.list = $("
").addClass(css.list),
p = this.pane = $('
').addClass(css.pane);
p.append(s).append(l);
if (o.parent) {
$(o.parent).append(p);
}
else {
p.css("position","absolute");
if (o.zIndex) {
p.css("z-index", o.zIndex);
}
$(document.body).append(p);
}
p.bind("mousedown", function(e) {
//console.log("pane mousedown");
self.input.data("dont_hide", true);
e.stopPropagation();
})
.bind("mouseup", function(e) {
//console.log("pane mouseup");
if (self.input.data("dont_hide")) {
self.input.focus();
}
self.input.removeData("dont_hide");
e.stopPropagation();
})
.bind("click", function(e) {
//console.log("pane click");
e.stopPropagation();
var s = self.get_selected();
if (s) {
self.onselect(s, true);
self.hide_all();
}
});
var hoverover = function(e) {
self.hoverover_list(e);
};
var hoverout = function(e) {
self.hoverout_list(e);
};
l.hover(hoverover, hoverout);
//console.log(this.pane, this.list);
this.input = $(input)
.attr("autocomplete", "off")
.unbind(".suggest")
.bind("remove.suggest", function(e) {
self._destroy();
})
.bind("keydown.suggest", function(e) {
self.keydown(e);
})
.bind("keypress.suggest", function(e) {
self.keypress(e);
})
.bind("keyup.suggest", function(e) {
self.keyup(e);
})
.bind("blur.suggest", function(e) {
self.blur(e);
})
.bind("textchange.suggest", function(e) {
self.textchange();
})
.bind("focus.suggest", function(e) {
self.focus(e);
})
.bind($.browser.msie ? "paste.suggest" : "input.suggest", function(e) {
clearTimeout(self.paste_timeout);
self.paste_timeout = setTimeout(function() {
self.textchange();
}, 0);
});
// resize handler
this.onresize = function(e) {
self.invalidate_position();
if (p.is(":visible")) {
self.position();
if (o.flyout && self.flyoutpane && self.flyoutpane.is(":visible")) {
var s = self.get_selected();
if (s) {
self.flyout_position(s);
}
}
}
};
$(window)
.bind("resize.suggest", this.onresize)
.bind("scroll.suggest", this.onresize);
};
$.suggest[name].prototype = $.extend({}, $.suggest.prototype, prototype);
};
// base suggest prototype
$.suggest.prototype = {
_init: function() {},
_destroy: function() {
this.pane.remove();
this.list.remove();
this.input.unbind(".suggest");
$(window)
.unbind("resize.suggest", this.onresize)
.unbind("scroll.suggest", this.onresize);
this.input.removeData("data.suggest");
},
invalidate_position: function() {
self._position = null;
},
status_start: function() {
this.hide_all();
this.status.siblings().hide();
if (this._status.START) {
this.status.text(this._status.START).show();
if (!this.pane.is(":visible")) {
this.position();
this.pane_show();
}
}
if (this._status.LOADING) {
this.status.removeClass("loading");
}
},
status_loading: function() {
this.status.siblings().show();
if (this._status.LOADING) {
this.status.addClass("loading").text(this._status.LOADING).show();
if (!this.pane.is(":visible")) {
this.position();
this.pane_show();
}
}
else {
this.status.hide();
}
},
status_select: function() {
this.status.siblings().show();
if (this._status.SELECT) {
this.status.text(this._status.SELECT).show();
}
else {
this.status.hide();
}
if (this._status.LOADING) {
this.status.removeClass("loading");
}
},
status_error: function() {
this.status.siblings().show();
if (this._status.ERROR) {
this.status.text(this._status.ERROR).show();
}
else {
this.status.hide();
}
if (this._status.LOADING) {
this.status.removeClass("loading");
}
},
focus: function(e) {
//console.log("focus", this.input.val() === "");
var o = this.options,
v = this.input.val();
if (v === "") {
this.status_start();
}
else {
this.focus_hook(e);
}
},
// override to be notified on focus and input has a value
focus_hook: function(e) {
//console.log("focus_hook", this.input.data("data.suggest"));
if (!this.input.data("data.suggest") &&
!this.pane.is(":visible") &&
$("." + this.options.css.item, this.list).length) {
this.position();
this.pane_show();
}
},
keydown: function(e) {
var key = e.keyCode;
if (key === 9) { // tab
this.tab(e);
}
else if (key === 38 || key === 40) { // up/down
if (!e.shiftKey) {
// prevents cursor/caret from moving (in Safari)
e.preventDefault();
}
}
},
keypress: function(e) {
var key = e.keyCode;
if (key === 38 || key === 40) { // up/down
if (!e.shiftKey) {
// prevents cursor/caret from moving
e.preventDefault();
}
}
else if (key === 13) { // enter
this.enter(e);
}
},
keyup: function(e) {
var key = e.keyCode;
//console.log("keyup", key);
if (key === 38) { // up
e.preventDefault();
this.up(e);
}
else if (key === 40) { // down
e.preventDefault();
this.down(e);
}
else if (e.ctrlKey && key === 77) {
$(".fbs-more-link", this.pane).click();
}
else if ($.suggest.is_char(e)) {
//this.textchange();
clearTimeout(this.keypress.timeout);
var self = this;
this.keypress.timeout = setTimeout(function() {
self.textchange();
}, 0);
}
else if (key === 27) {
// escape - WebKit doesn't fire keypress for escape
this.escape(e);
}
return true;
},
blur: function(e) {
//console.log("blur", "dont_hide", this.input.data("dont_hide"),
// "data.suggest", this.input.data("data.suggest"));
if (this.input.data("dont_hide")) {
return;
}
var data = this.input.data("data.suggest");
this.hide_all();
},
tab: function(e) {
if (e.shiftKey || e.metaKey || e.ctrlKey) {
return;
}
var o = this.options,
visible = this.pane.is(":visible") &&
$("." + o.css.item, this.list).length,
s = this.get_selected();
//console.log("tab", visible, s);
if (visible && s) {
this.onselect(s);
this.hide_all();
}
},
enter: function(e) {
var o = this.options,
visible = this.pane.is(":visible");
//console.log("enter", visible);
if (visible) {
if (e.shiftKey) {
this.shift_enter(e);
e.preventDefault();
return;
}
else if ($("." + o.css.item, this.list).length) {
var s = this.get_selected();
if (s) {
this.onselect(s);
this.hide_all();
e.preventDefault();
return;
}
else if (!o.soft) {
var data = this.input.data("data.suggest");
if ($("."+this.options.css.item + ":visible", this.list).length) {
this.updown(false);
e.preventDefault();
return;
}
}
}
}
if (o.soft) {
// submit form
this.soft_enter();
}
else {
e.preventDefault();
}
},
soft_enter: function(e) {},
shift_enter: function(e) {},
escape: function(e) {
this.hide_all();
},
up: function(e) {
//console.log("up");
this.updown(true, e.ctrlKey || e.shiftKey);
},
down: function(e) {
//console.log("up");
this.updown(false, null, e.ctrlKey || e.shiftKey);
},
updown: function(goup, gofirst, golast) {
//console.log("updown", goup, gofirst, golast);
var o = this.options,
css = o.css,
p = this.pane,
l = this.list;
if (!p.is(":visible")) {
if (!goup) {
this.textchange();
}
return;
}
var li = $("."+css.item + ":visible", l);
if (!li.length) {
return;
}
var first = $(li[0]),
last = $(li[li.length-1]),
cur = this.get_selected() || [];
clearTimeout(this.ignore_mouseover.timeout);
this._ignore_mouseover = false;
if (goup) {//up
if (gofirst) {
this._goto(first);
}
else if (!cur.length) {
this._goto(last);
}
else if (cur[0] == first[0]) {
first.removeClass(css.selected);
this.input.val(this.input.data("original.suggest"));
this.hoverout_list();
}
else {
var prev = cur.prevAll("."+css.item + ":visible:first");
this._goto(prev);
}
}
else {//down
if (golast) {
this._goto(last);
}
else if (!cur.length) {
this._goto(first);
}
else if (cur[0] == last[0]) {
last.removeClass(css.selected);
this.input.val(this.input.data("original.suggest"));
this.hoverout_list();
}
else {
var next = cur.nextAll("."+css.item + ":visible:first");
this._goto(next);
}
}
},
_goto: function(li) {
li.trigger("mouseover.suggest");
var d = li.data("data.suggest");
this.input.val(d ? d.name : this.input.data("original.suggest"));
this.scroll_to(li);
},
scroll_to: function(item) {
var l = this.list,
scrollTop = l.scrollTop(),
scrollBottom = scrollTop + l.innerHeight(),
item_height = item.outerHeight(),
offsetTop = item.prevAll().length * item_height,
offsetBottom = offsetTop + item_height;
if (offsetTop < scrollTop) {
this.ignore_mouseover();
l.scrollTop(offsetTop);
}
else if (offsetBottom > scrollBottom) {
this.ignore_mouseover();
l.scrollTop(scrollTop + offsetBottom - scrollBottom);
}
},
textchange: function() {
this.input.removeData("data.suggest");
this.input.trigger("fb-textchange", this);
var v = this.input.val();
if (v === "") {
this.status_start();
return;
}
else {
this.status_loading();
}
this.request(v);
},
request: function() {},
response: function(data) {
if (!data) {
return;
}
if ("cost" in data) {
this.trackEvent(this.name, "response", "cost", data.cost);
}
if (!this.check_response(data)) {
return;
}
var result = [];
if ($.isArray(data)) {
result = data;
}
else if ("result" in data) {
result = data.result;
}
var args = $.map(arguments, function(a) {
return a;
});
this.response_hook.apply(this, args);
var first = null,
self = this,
o = this.options;
$.each(result, function(i,n) {
if (!n.id && n.mid) {
// For compatitibility reasons, store the mid as id
n.id = n.mid;
}
var li = self.create_item(n, data)
.bind("mouseover.suggest", function(e) {
self.mouseover_item(e);
});
li.data("data.suggest", n);
self.list.append(li);
if (i === 0) {
first = li;
}
});
this.input.data("original.suggest", this.input.val());
if ($("."+o.css.item, this.list).length === 0 && o.nomatch) {
var $nomatch = $('
');
if (typeof o.nomatch === "string") {
$nomatch.text(o.nomatch);
}
else {
if (o.nomatch.title) {
$nomatch.append($('').text(o.nomatch.title));
}
if (o.nomatch.heading) {
$nomatch.append($('
').text(o.nomatch.heading));
}
var tips = o.nomatch.tips;
if (tips && tips.length) {
var $tips = $('