c9c9ee7509
Closes #1381
1953 lines
56 KiB
JavaScript
1953 lines
56 KiB
JavaScript
/*
|
|
* 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 = $("<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 = $('<div style="display:none;">').addClass(css.status),
|
|
l = this.list = $("<ul>").addClass(css.list),
|
|
p = this.pane = $('<div style="display:none;" class="fbs-reset">').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 = $('<li class="fbs-nomatch">');
|
|
if (typeof o.nomatch === "string") {
|
|
$nomatch.text(o.nomatch);
|
|
}
|
|
else {
|
|
if (o.nomatch.title) {
|
|
$nomatch.append($('<em class="fbs-nomatch-text">').text(o.nomatch.title));
|
|
}
|
|
if (o.nomatch.heading) {
|
|
$nomatch.append($('<h3>').text(o.nomatch.heading));
|
|
}
|
|
var tips = o.nomatch.tips;
|
|
if (tips && tips.length) {
|
|
var $tips = $('<ul class="fbs-search-tips">');
|
|
$.each(tips, function(i,tip) {
|
|
$tips.append($("<li>").text(tip));
|
|
});
|
|
$nomatch.append($tips);
|
|
}
|
|
}
|
|
$nomatch.bind("click.suggest", function(e) {
|
|
e.stopPropagation();
|
|
});
|
|
this.list.append($nomatch);
|
|
}
|
|
|
|
args.push(first);
|
|
this.show_hook.apply(this, args);
|
|
this.position();
|
|
this.pane_show();
|
|
},
|
|
|
|
pane_show: function() {
|
|
var show = false;
|
|
if ($("> li", this.list).length) {
|
|
show = true;
|
|
}
|
|
if (!show) {
|
|
this.pane.children(":not(." + this.options.css.list + ")")
|
|
.each(function() {
|
|
if ($(this).css("display") != "none") {
|
|
show = true;
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
if (show) {
|
|
if (this.options.animate) {
|
|
var self = this;
|
|
this.pane.slideDown("fast", function() {
|
|
self.input.trigger("fb-pane-show", self);
|
|
});
|
|
}
|
|
else {
|
|
this.pane.show();
|
|
this.input.trigger("fb-pane-show", this);
|
|
}
|
|
}
|
|
else {
|
|
this.pane.hide();
|
|
this.input.trigger("fb-pane-hide", this);
|
|
}
|
|
},
|
|
|
|
create_item: function(data, response_data) {
|
|
var css = this.options.css;
|
|
var li = $("<li>").addClass(css.item);
|
|
var label = $("<label>").text(data.name);
|
|
li.append($("<div>").addClass(css.item_name).append(label));
|
|
return li;
|
|
},
|
|
|
|
mouseover_item: function(e) {
|
|
if (this._ignore_mouseover) {
|
|
return;
|
|
}
|
|
var target = e.target;
|
|
if (target.nodeName.toLowerCase() !== "li") {
|
|
target = $(target).parents("li:first");
|
|
}
|
|
var li = $(target),
|
|
css = this.options.css,
|
|
l = this.list;
|
|
$("."+css.item, l)
|
|
.each(function() {
|
|
if (this !== li[0]) {
|
|
$(this).removeClass(css.selected);
|
|
}
|
|
});
|
|
if (!li.hasClass(css.selected)) {
|
|
li.addClass(css.selected);
|
|
this.mouseover_item_hook(li);
|
|
}
|
|
},
|
|
|
|
mouseover_item_hook: function($li) {},
|
|
|
|
hoverover_list: function(e) {},
|
|
|
|
hoverout_list: function(e) {},
|
|
|
|
check_response: function(response_data) {
|
|
return true;
|
|
},
|
|
|
|
response_hook: function(response_data) {
|
|
//this.pane.hide();
|
|
this.list.empty();
|
|
},
|
|
|
|
show_hook: function(response_data) {
|
|
// remove anything next to list - added by other suggest plugins
|
|
this.status_select();
|
|
},
|
|
|
|
position: function() {
|
|
var p = this.pane,
|
|
o = this.options;
|
|
|
|
if (o.parent) {
|
|
return;
|
|
}
|
|
|
|
if (!self._position) {
|
|
var inp = this.input,
|
|
pos = inp.offset(),
|
|
input_width = inp.outerWidth(true),
|
|
input_height = inp.outerHeight(true);
|
|
pos.top += input_height;
|
|
|
|
// show to calc dimensions
|
|
var pane_width = p.outerWidth(),
|
|
pane_height = p.outerHeight(),
|
|
pane_right = pos.left + pane_width,
|
|
pane_bottom = pos.top + pane_height,
|
|
pane_half = pos.top + pane_height / 2,
|
|
scroll_left = $(window).scrollLeft(),
|
|
scroll_top = $(window).scrollTop(),
|
|
window_width = $(window).width(),
|
|
window_height = $(window).height(),
|
|
window_right = window_width + scroll_left,
|
|
window_bottom = window_height + scroll_top;
|
|
|
|
// is input left or right side of window?
|
|
var left = true;
|
|
if ('left' == o.align ) {
|
|
left = true;
|
|
}
|
|
else if ('right' == o.align ) {
|
|
left = false;
|
|
}
|
|
else if (pos.left > (scroll_left + window_width/2)) {
|
|
left = false;
|
|
}
|
|
if (!left) {
|
|
left = pos.left - (pane_width - input_width);
|
|
if (left > scroll_left) {
|
|
pos.left = left;
|
|
}
|
|
}
|
|
|
|
if (pane_half > window_bottom) {
|
|
// can we see at least half of the list?
|
|
var top = pos.top - input_height - pane_height;
|
|
if (top > scroll_top) {
|
|
pos.top = top;
|
|
}
|
|
}
|
|
this._position = pos;
|
|
}
|
|
p.css({top:this._position.top, left:this._position.left});
|
|
},
|
|
|
|
ignore_mouseover: function(e) {
|
|
this._ignore_mouseover = true;
|
|
var self = this;
|
|
this.ignore_mouseover.timeout =
|
|
setTimeout(function() {
|
|
self.ignore_mouseover_reset();
|
|
}, 1000);
|
|
},
|
|
|
|
ignore_mouseover_reset: function() {
|
|
this._ignore_mouseover = false;
|
|
},
|
|
|
|
get_selected: function() {
|
|
var selected = null,
|
|
select_class = this.options.css.selected;
|
|
$("li", this.list)
|
|
.each(function() {
|
|
var $this = $(this);
|
|
if ($this.hasClass(select_class) &&
|
|
$this.is(":visible")) {
|
|
selected = $this;
|
|
return false;
|
|
}
|
|
});
|
|
return selected;
|
|
},
|
|
|
|
onselect: function($selected, focus) {
|
|
var data = $selected.data("data.suggest");
|
|
if (data) {
|
|
this.input.val(data.name)
|
|
.data("data.suggest", data)
|
|
.trigger("fb-select", data);
|
|
|
|
this.trackEvent(this.name, "fb-select", "index",
|
|
$selected.prevAll().length);
|
|
}
|
|
},
|
|
|
|
trackEvent: function(category, action, label, value) {
|
|
this.input.trigger("fb-track-event", {
|
|
category: category,
|
|
action:action,
|
|
label: label,
|
|
value: value
|
|
});
|
|
//console.log("trackEvent", category, action, label, value);
|
|
},
|
|
|
|
hide_all: function(e) {
|
|
this.pane.hide();
|
|
this.input.trigger("fb-pane-hide", this);
|
|
}
|
|
|
|
};
|
|
|
|
|
|
$.extend($.suggest, {
|
|
|
|
defaults: {
|
|
|
|
status: [
|
|
'Start typing to get suggestions...',
|
|
'Searching...',
|
|
'Select an item from the list:',
|
|
'Sorry, something went wrong. Please try again later'
|
|
],
|
|
|
|
soft: false,
|
|
|
|
nomatch: "no matches",
|
|
|
|
// CSS default class names
|
|
css: {
|
|
pane: "fbs-pane",
|
|
list: "fbs-list",
|
|
item: "fbs-item",
|
|
item_name: "fbs-item-name",
|
|
selected: "fbs-selected",
|
|
status: "fbs-status"
|
|
},
|
|
|
|
css_prefix: null,
|
|
|
|
parent: null,
|
|
|
|
// option to animate suggest list when shown
|
|
animate: false,
|
|
|
|
zIndex: null
|
|
},
|
|
|
|
strongify: function(str, substr) {
|
|
// safely markup substr within str with <strong>
|
|
var strong;
|
|
var index = str.toLowerCase().indexOf(substr.toLowerCase());
|
|
if (index >= 0) {
|
|
var substr_len = substr.length;
|
|
var pre = document.createTextNode(str.substring(0, index));
|
|
var em = $("<strong>").text(str.substring(index, index + substr_len));
|
|
var post = document.createTextNode(str.substring(index + substr_len));
|
|
strong = $("<div>")
|
|
.append(pre).append(em).append(post);
|
|
}
|
|
else {
|
|
strong = $("<div>").text(str);
|
|
}
|
|
return strong;
|
|
},
|
|
|
|
keyCode: {
|
|
//BACKSPACE: 8,
|
|
CAPS_LOCK: 20,
|
|
//COMMA: 188,
|
|
CONTROL: 17,
|
|
//DELETE: 46,
|
|
DOWN: 40,
|
|
END: 35,
|
|
ENTER: 13,
|
|
ESCAPE: 27,
|
|
HOME: 36,
|
|
INSERT: 45,
|
|
LEFT: 37,
|
|
//NUMPAD_ADD: 107,
|
|
//NUMPAD_DECIMAL: 110,
|
|
//NUMPAD_DIVIDE: 111,
|
|
NUMPAD_ENTER: 108,
|
|
//NUMPAD_MULTIPLY: 106,
|
|
//NUMPAD_SUBTRACT: 109,
|
|
PAGE_DOWN: 34,
|
|
PAGE_UP: 33,
|
|
//PERIOD: 190,
|
|
RIGHT: 39,
|
|
SHIFT: 16,
|
|
SPACE: 32,
|
|
TAB: 9,
|
|
UP: 38,
|
|
OPTION: 18,
|
|
APPLE: 224
|
|
},
|
|
|
|
is_char: function(e) {
|
|
if (e.type === "keypress") {
|
|
if ((e.metaKey || e.ctrlKey) && e.charCode === 118) {
|
|
// ctrl+v
|
|
return true;
|
|
}
|
|
else if ("isChar" in e) {
|
|
return e.isChar;
|
|
}
|
|
}
|
|
else {
|
|
var not_char = $.suggest.keyCode.not_char;
|
|
if (!not_char) {
|
|
not_char = {};
|
|
$.each($.suggest.keyCode, function(k,v) {
|
|
not_char[''+v] = 1;
|
|
});
|
|
$.suggest.keyCode.not_char = not_char;
|
|
}
|
|
return !(('' + e.keyCode) in not_char);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Parse input string into actual query string and structured name:value list
|
|
*
|
|
* "bob dylan type:artist" -> ["bob dylan", ["type:artist"]]
|
|
* "Dear... type:film name{full}:Dear..." -> ["Dear...", ["type:film", "name{full}:Dear..."]]
|
|
*/
|
|
parse_input: function(str) {
|
|
// only pick out valid name:value pairs
|
|
// a name:value is valid
|
|
// 1. if there are no spaces before/after ":"
|
|
// 2. name does not have any spaces
|
|
// 3. value does not have any spaces OR value is double quoted
|
|
var regex = /(\S+)\:(?:\"([^\"]+)\"|(\S+))/g;
|
|
var qstr = str;
|
|
var filters = [];
|
|
var overrides = {};
|
|
var m = regex.exec(str);
|
|
while (m) {
|
|
if (m[1] in SEARCH_PARAMS) {
|
|
overrides[m[1]] = $.isEmptyObject(m[2]) ? m[3] : m[2];
|
|
}
|
|
else {
|
|
filters.push(m[0]);
|
|
}
|
|
qstr = qstr.replace(m[0], "");
|
|
m = regex.exec(str);
|
|
}
|
|
qstr = $.trim(qstr.replace(/\s+/g, " "));
|
|
return [qstr, filters, overrides];
|
|
},
|
|
|
|
/**
|
|
* Convenient methods and regexs to determine valid mql ids.
|
|
*/
|
|
mqlkey_fast: /^[_A-Za-z0-9][A-Za-z0-9_-]*$/,
|
|
mqlkey_slow: /^(?:[A-Za-z0-9]|\$[A-F0-9]{4})(?:[A-Za-z0-9_-]|\$[A-F0-9]{4})*$/,
|
|
check_mql_key: function(val) {
|
|
if ($.suggest.mqlkey_fast.test(val)) {
|
|
return true;
|
|
}
|
|
else if ($.suggest.mqlkey_slow.test(val)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
check_mql_id: function(val) {
|
|
if (val.indexOf("/") === 0) {
|
|
var keys = val.split("/");
|
|
// remove beginning '/'
|
|
keys.shift();
|
|
if (keys.length == 1 && keys[0] === "") {
|
|
// "/" is a valid id
|
|
return true;
|
|
}
|
|
else {
|
|
for (var i=0,l=keys.length; i<l; i++) {
|
|
if (!$.suggest.check_mql_key(keys[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
is_system_type: function(type_id) {
|
|
if (type_id == null) {
|
|
return false;
|
|
}
|
|
return (type_id.indexOf("/type/") === 0);
|
|
}
|
|
});
|
|
|
|
|
|
// some base implementation that we overwrite but want to call
|
|
var base = {
|
|
_destroy: $.suggest.prototype._destroy,
|
|
show_hook: $.suggest.prototype.show_hook
|
|
};
|
|
|
|
|
|
// *THE* Freebase suggest implementation
|
|
$.suggest("suggest", {
|
|
_init: function() {
|
|
var self = this,
|
|
o = this.options;
|
|
if (o.flyout_service_url == null) {
|
|
o.flyout_service_url = o.service_url;
|
|
}
|
|
this.flyout_url = o.flyout_service_url;
|
|
if (o.flyout_service_path) {
|
|
this.flyout_url += o.flyout_service_path;
|
|
}
|
|
// set api key for flyout service (search)
|
|
this.flyout_url = this.flyout_url.replace(/\$\{key\}/g, o.key);
|
|
if (o.flyout_image_service_url == null) {
|
|
o.flyout_image_service_url = o.service_url;
|
|
}
|
|
this.flyout_image_url = o.flyout_image_service_url;
|
|
if (o.flyout_image_service_path) {
|
|
this.flyout_image_url += o.flyout_image_service_path;
|
|
}
|
|
// set api key for image api
|
|
this.flyout_image_url = this.flyout_image_url.replace(/\$\{key\}/g, o.key);
|
|
|
|
if (!$.suggest.cache) {
|
|
$.suggest.cache = {};
|
|
}
|
|
|
|
if (o.flyout) {
|
|
this.flyoutpane = $('<div style="display:none;" class="fbs-reset">')
|
|
.addClass(o.css.flyoutpane);
|
|
|
|
if (o.flyout_parent) {
|
|
$(o.flyout_parent).append(this.flyoutpane);
|
|
}
|
|
else {
|
|
this.flyoutpane.css("position","absolute");
|
|
if (o.zIndex) {
|
|
this.flyoutpane.css("z-index", o.zIndex);
|
|
}
|
|
$(document.body).append(this.flyoutpane);
|
|
}
|
|
var hoverover = function(e) {
|
|
self.hoverover_list(e);
|
|
};
|
|
var hoverout = function(e) {
|
|
self.hoverout_list(e);
|
|
};
|
|
this.flyoutpane.hover(hoverover, hoverout)
|
|
.bind("mousedown.suggest", function(e) {
|
|
e.stopPropagation();
|
|
self.pane.click();
|
|
});
|
|
|
|
if (!$.suggest.flyout) {
|
|
$.suggest.flyout = {};
|
|
}
|
|
if (!$.suggest.flyout.cache) {
|
|
$.suggest.flyout.cache = {};
|
|
}
|
|
}
|
|
},
|
|
|
|
_destroy: function() {
|
|
base._destroy.call(this);
|
|
if (this.flyoutpane) {
|
|
this.flyoutpane.remove();
|
|
}
|
|
this.input.removeData("request.count.suggest");
|
|
this.input.removeData("flyout.request.count.suggest");
|
|
},
|
|
|
|
shift_enter: function(e) {
|
|
if (this.options.suggest_new) {
|
|
this.suggest_new();
|
|
this.hide_all();
|
|
}
|
|
},
|
|
|
|
hide_all: function(e) {
|
|
this.pane.hide();
|
|
if (this.flyoutpane) {
|
|
this.flyoutpane.hide();
|
|
}
|
|
this.input.trigger("fb-pane-hide", this);
|
|
this.input.trigger("fb-flyoutpane-hide", this);
|
|
},
|
|
|
|
request: function(val, cursor) {
|
|
var self = this,
|
|
o = this.options;
|
|
|
|
var query = val;
|
|
|
|
var filter = o.ac_param.filter || [];
|
|
|
|
// SEARCH_PARAMS can be overridden inline
|
|
var extend_ac_param = null;
|
|
|
|
if ($.type(filter) === "string") {
|
|
// the original filter may be a single filter param (string)
|
|
filter = [filter];
|
|
}
|
|
// clone original filters so that we don't modify it
|
|
filter = filter.slice();
|
|
if (o.advanced) {
|
|
// parse out additional filters in input value
|
|
var structured = $.suggest.parse_input(query);
|
|
query = structured[0];
|
|
if (structured[1].length) {
|
|
// all advance filters are ANDs
|
|
filter.push("(all " + structured[1].join(" ") + ")");
|
|
}
|
|
extend_ac_param = structured[2];
|
|
if ($.suggest.check_mql_id(query)) {
|
|
// handle anything that looks like a valid mql id:
|
|
// filter=(all%20alias{start}:/people/pers)&prefixed=true
|
|
filter.push("(any alias{start}:\"" + query + "\" mid:\"" +
|
|
query + "\")");
|
|
extend_ac_param['prefixed'] = true;
|
|
query = "";
|
|
}
|
|
}
|
|
|
|
var data = {};
|
|
data[o.query_param_name] = query;
|
|
|
|
if (cursor) {
|
|
data.cursor = cursor;
|
|
}
|
|
$.extend(data, o.ac_param, extend_ac_param);
|
|
if (filter.length) {
|
|
data.filter = filter;
|
|
}
|
|
|
|
var url = o.service_url + o.service_path + "?" + $.param(data, true);
|
|
var cached = $.suggest.cache[url];
|
|
if (cached) {
|
|
this.response(cached, cursor ? cursor : -1, true);
|
|
return;
|
|
}
|
|
|
|
clearTimeout(this.request.timeout);
|
|
|
|
var ajax_options = {
|
|
url: o.service_url + o.service_path,
|
|
data: data,
|
|
traditional: true,
|
|
beforeSend: function(xhr) {
|
|
var calls = self.input.data("request.count.suggest") || 0;
|
|
if (!calls) {
|
|
self.trackEvent(self.name, "start_session");
|
|
}
|
|
calls += 1;
|
|
self.trackEvent(self.name, "request", "count", calls);
|
|
self.input.data("request.count.suggest", calls);
|
|
},
|
|
success: function(data) {
|
|
$.suggest.cache[url] = data;
|
|
data.prefix = val; // keep track of prefix to match up response with input value
|
|
self.response(data, cursor ? cursor : -1);
|
|
},
|
|
error: function(xhr) {
|
|
self.status_error();
|
|
self.trackEvent(self.name, "request", "error", {
|
|
url: this.url,
|
|
response: xhr ? xhr.responseText : ''
|
|
});
|
|
self.input.trigger("fb-error", Array.prototype.slice.call(arguments));
|
|
},
|
|
complete: function(xhr) {
|
|
if (xhr) {
|
|
self.trackEvent(self.name, "request", "tid",
|
|
xhr.getResponseHeader("X-Metaweb-TID"));
|
|
}
|
|
},
|
|
dataType: "jsonp",
|
|
cache: true
|
|
};
|
|
|
|
this.request.timeout = setTimeout(function() {
|
|
$.ajax(ajax_options);
|
|
}, o.xhr_delay);
|
|
},
|
|
|
|
create_item: function(data, response_data) {
|
|
var css = this.options.css;
|
|
var li = $("<li>").addClass(css.item);
|
|
var label = $("<label>")
|
|
.append($.suggest.strongify(data.name || data.id, response_data.prefix));
|
|
var name = $("<div>").addClass(css.item_name)
|
|
.append(label);
|
|
var nt = data.notable;
|
|
if (data.under) {
|
|
$(":first", label).append($("<small>").text(" ("+data.under+")"));
|
|
}
|
|
if ((nt != null && $.suggest.is_system_type(nt.id)) ||
|
|
(this.options.scoring != null &&
|
|
this.options.scoring.toUpperCase() === 'SCHEMA')) {
|
|
$(":first", label).append($("<small>").text(" ("+data.id+")"));
|
|
}
|
|
var types = data.type;
|
|
li.append(name);
|
|
var type = $("<div>").addClass(css.item_type);
|
|
if (nt && nt.name) {
|
|
type.text(nt.name);
|
|
}
|
|
else if (this.options.show_id && data.id) {
|
|
// display human readable id if no notable type
|
|
type.text(data.id);
|
|
}
|
|
name.prepend(type);
|
|
|
|
//console.log("create_item", li);
|
|
return li;
|
|
},
|
|
|
|
|
|
mouseover_item_hook: function(li) {
|
|
var data = li.data("data.suggest");
|
|
if (this.options.flyout) {
|
|
if (data) {
|
|
this.flyout_request(data);
|
|
}
|
|
else {
|
|
//this.flyoutpane.hide();
|
|
}
|
|
}
|
|
},
|
|
|
|
check_response: function(response_data) {
|
|
return response_data.prefix === this.input.val();
|
|
},
|
|
|
|
response_hook: function(response_data, cursor) {
|
|
if (this.flyoutpane) {
|
|
this.flyoutpane.hide();
|
|
}
|
|
if (cursor > 0) {
|
|
$(".fbs-more", this.pane).remove();
|
|
}
|
|
else {
|
|
//this.pane.hide();
|
|
this.list.empty();
|
|
}
|
|
},
|
|
|
|
show_hook: function(response_data, cursor, first) {
|
|
base.show_hook.apply(this, [response_data]);
|
|
|
|
var o = this.options,
|
|
self = this,
|
|
p = this.pane,
|
|
l = this.list,
|
|
result = response_data.result,
|
|
more = $(".fbs-more", p),
|
|
suggestnew = $(".fbs-suggestnew", p),
|
|
status = $(".fbs-status", p);
|
|
|
|
// spell/correction
|
|
var correction = response_data.correction;
|
|
if (correction && correction.length) {
|
|
var spell_link = $('<a class="fbs-spell-link" href="#">').append(correction[0])
|
|
.bind("click.suggest", function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
self.input.val(correction[0]).trigger("textchange");
|
|
});
|
|
self.status
|
|
.empty()
|
|
.append("Search instead for ")
|
|
.append(spell_link)
|
|
.show();
|
|
}
|
|
|
|
// more
|
|
if (result && result.length && "cursor" in response_data) {
|
|
if (!more.length) {
|
|
var more_link = $('<a class="fbs-more-link" href="#" title="(Ctrl+m)">view more</a>');
|
|
more = $('<div class="fbs-more">').append(more_link);
|
|
more_link.bind("click.suggest", function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
var m = $(this).parent(".fbs-more");
|
|
self.more(m.data("cursor.suggest"));
|
|
});
|
|
l.after(more);
|
|
}
|
|
more.data("cursor.suggest", response_data.cursor);
|
|
more.show();
|
|
}
|
|
else {
|
|
more.remove();
|
|
}
|
|
|
|
// suggest_new
|
|
if (o.suggest_new) {
|
|
if (!suggestnew.length) {
|
|
// create suggestnew option
|
|
var button = $('<button class="fbs-suggestnew-button">');
|
|
button.text(o.suggest_new);
|
|
suggestnew = $('<div class="fbs-suggestnew">')
|
|
.append('<div class="fbs-suggestnew-description">Your item not in the list?</div>')
|
|
.append(button)
|
|
.append('<span class="fbs-suggestnew-shortcut">(Shift+Enter)</span>')
|
|
.bind("click.suggest", function(e) {
|
|
e.stopPropagation();
|
|
self.suggest_new(e);
|
|
});
|
|
p.append(suggestnew);
|
|
}
|
|
suggestnew.show();
|
|
}
|
|
else {
|
|
suggestnew.remove();
|
|
}
|
|
|
|
// scroll to first if clicked on "more"
|
|
if (first && first.length && cursor > 0) {
|
|
var top = first.prevAll().length * first.outerHeight();
|
|
var scrollTop = l.scrollTop();
|
|
l.animate({scrollTop: top}, "slow", function() {
|
|
first.trigger("mouseover.suggest");
|
|
});
|
|
}
|
|
},
|
|
|
|
suggest_new: function(e) {
|
|
var v = this.input.val();
|
|
if (v === "") {
|
|
return;
|
|
}
|
|
//console.log("suggest_new", v);
|
|
this.input
|
|
.data("data.suggest", v)
|
|
.trigger("fb-select-new", v);
|
|
this.trackEvent(this.name, "fb-select-new", "index", "new");
|
|
this.hide_all();
|
|
},
|
|
|
|
more: function(cursor) {
|
|
if (cursor) {
|
|
var orig = this.input.data("original.suggest");
|
|
if (orig !== null) {
|
|
this.input.val(orig);
|
|
}
|
|
this.request(this.input.val(), cursor);
|
|
this.trackEvent(this.name, "more", "cursor", cursor);
|
|
}
|
|
return false;
|
|
},
|
|
|
|
flyout_request: function(data) {
|
|
var self = this;
|
|
var o = this.options;
|
|
var sug_data = this.flyoutpane.data("data.suggest");
|
|
if (sug_data && data.id === sug_data.id) {
|
|
if (!this.flyoutpane.is(":visible")) {
|
|
var s = this.get_selected();
|
|
this.flyout_position(s);
|
|
this.flyoutpane.show();
|
|
this.input.trigger("fb-flyoutpane-show", this);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// check $.suggest.flyout.cache
|
|
var cached = $.suggest.flyout.cache[data.id];
|
|
if (cached && cached.id && cached.html) {
|
|
// CLI-10009: use cached item only if id and html present
|
|
this.flyout_response(cached);
|
|
return;
|
|
}
|
|
|
|
//this.flyoutpane.hide();
|
|
var flyout_id = data.id;
|
|
var url = this.flyout_url.replace(/\$\{id\}/g, encodeURIComponent(data.id));
|
|
|
|
var ajax_options = {
|
|
url: url,
|
|
traditional: true,
|
|
beforeSend: function(xhr) {
|
|
var calls = self.input.data("flyout.request.count.suggest") || 0;
|
|
calls += 1;
|
|
self.trackEvent(self.name, "flyout.request", "count", calls);
|
|
self.input.data("flyout.request.count.suggest", calls);
|
|
},
|
|
success: function(data) {
|
|
data["req:id"] = flyout_id;
|
|
if (data['result'] && data['result'].length) {
|
|
data.html =
|
|
$.suggest.suggest.create_flyout(data['result'][0],
|
|
self.flyout_image_url);
|
|
}
|
|
$.suggest.flyout.cache[flyout_id] = data;
|
|
self.flyout_response(data);
|
|
},
|
|
error: function(xhr) {
|
|
self.trackEvent(self.name, "flyout", "error", {
|
|
url:this.url,
|
|
response: xhr ? xhr.responseText : ''
|
|
});
|
|
},
|
|
complete: function(xhr) {
|
|
if (xhr) {
|
|
self.trackEvent(self.name, "flyout", "tid",
|
|
xhr.getResponseHeader("X-Metaweb-TID"));
|
|
}
|
|
},
|
|
dataType: "jsonp",
|
|
cache: true
|
|
};
|
|
if (o.flyout_lang) {
|
|
ajax_options.data = {lang:o.flyout_lang};
|
|
}
|
|
|
|
clearTimeout(this.flyout_request.timeout);
|
|
this.flyout_request.timeout =
|
|
setTimeout(function() {
|
|
$.ajax(ajax_options);
|
|
}, o.xhr_delay);
|
|
|
|
this.input.trigger("fb-request-flyout", ajax_options);
|
|
},
|
|
|
|
flyout_response: function(data) {
|
|
var o = this.options,
|
|
p = this.pane,
|
|
s = this.get_selected() || [];
|
|
if (p.is(":visible") && s.length) {
|
|
var sug_data = s.data("data.suggest");
|
|
if (sug_data && data["req:id"] === sug_data.id && data.html) {
|
|
this.flyoutpane.html(data.html);
|
|
this.flyout_position(s);
|
|
this.flyoutpane.show()
|
|
.data("data.suggest", sug_data);
|
|
this.input.trigger("fb-flyoutpane-show", this);
|
|
}
|
|
}
|
|
},
|
|
|
|
flyout_position: function($item) {
|
|
if (this.options.flyout_parent) {
|
|
return;
|
|
}
|
|
|
|
var p = this.pane,
|
|
fp = this.flyoutpane,
|
|
css = this.options.css,
|
|
pos = undefined,
|
|
old_pos = {
|
|
top: parseInt(fp.css("top"), 10),
|
|
left: parseInt(fp.css("left"), 10)
|
|
},
|
|
pane_pos = p.offset(),
|
|
pane_width = p.outerWidth(),
|
|
flyout_height = fp.outerHeight(),
|
|
flyout_width = fp.outerWidth();
|
|
|
|
if (this.options.flyout === "bottom") {
|
|
// flyout position on top/bottom
|
|
pos = pane_pos;
|
|
var input_pos = this.input.offset();
|
|
if (pane_pos.top < input_pos.top) {
|
|
pos.top -= flyout_height;
|
|
}
|
|
else {
|
|
pos.top += p.outerHeight();
|
|
}
|
|
fp.addClass(css.flyoutpane + "-bottom");
|
|
}
|
|
else {
|
|
pos = $item.offset();
|
|
var item_height = $item.outerHeight();
|
|
|
|
pos.left += pane_width;
|
|
var flyout_right = pos.left + flyout_width,
|
|
scroll_left = $(document.body).scrollLeft(),
|
|
window_right = $(window).width() + scroll_left;
|
|
|
|
pos.top = pos.top + item_height - flyout_height;
|
|
if (pos.top < pane_pos.top) {
|
|
pos.top = pane_pos.top;
|
|
}
|
|
|
|
if (flyout_right > window_right) {
|
|
var left = pos.left - (pane_width + flyout_width);
|
|
if (left > scroll_left) {
|
|
pos.left = left;
|
|
}
|
|
}
|
|
fp.removeClass(css.flyoutpane + "-bottom");
|
|
}
|
|
|
|
if (!(pos.top === old_pos.top &&
|
|
pos.left === old_pos.left)) {
|
|
fp.css({top:pos.top, left:pos.left});
|
|
}
|
|
},
|
|
|
|
hoverout_list: function(e) {
|
|
if (this.flyoutpane && !this.get_selected()) {
|
|
this.flyoutpane.hide();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Freebase suggest settings
|
|
$.extend($.suggest.suggest, {
|
|
|
|
defaults: {
|
|
/**
|
|
* filter, spell, lang, exact, scoring, key, prefixed, stemmed, format
|
|
*
|
|
* are the new parameters used by the new freebase search on googleapis.
|
|
* Please refer the the API documentation as these parameters
|
|
* will be transparently passed through to the search service.
|
|
*
|
|
* @see http://wiki.freebase.com/wiki/ApiSearch
|
|
*/
|
|
|
|
// search filters
|
|
filter: null,
|
|
|
|
// spelling corrections
|
|
spell: "always",
|
|
|
|
exact: false,
|
|
|
|
scoring: null,
|
|
|
|
// language to search (default to en)
|
|
lang: null, // NULL defaults to "en",
|
|
|
|
// API key: required for googleapis
|
|
key: null,
|
|
|
|
prefixed: true,
|
|
|
|
stemmed: null,
|
|
|
|
format: null,
|
|
|
|
// Enable structured input name:value pairs that get appended to the search filters
|
|
// For example:
|
|
//
|
|
// "bob dylan type:artist"
|
|
//
|
|
// Would get translated to the following request:
|
|
//
|
|
// /freebase/v1/search?query=bob+dylan&filter=<original filter>&filter=(all type:artist)
|
|
//
|
|
advanced: true,
|
|
|
|
// If an item does not have a "notable" field, display the id or mid of the item
|
|
show_id: true,
|
|
|
|
// query param name for the search service.
|
|
// If query name was "foo": search?foo=...
|
|
query_param_name: "query",
|
|
|
|
// base url for autocomplete service
|
|
service_url: "https://www.googleapis.com/freebase/v1",
|
|
|
|
// service_url + service_path = url to autocomplete service
|
|
service_path: "/search",
|
|
|
|
// 'left', 'right' or null
|
|
// where list will be aligned left or right with the input
|
|
align: null,
|
|
|
|
// whether or not to show flyout on mouseover
|
|
flyout: true,
|
|
|
|
// default is service_url if NULL
|
|
flyout_service_url: null,
|
|
|
|
// flyout_service_url + flyout_service_path =
|
|
// url to search with
|
|
// output=(notable:/client/summary (description citation) type).
|
|
flyout_service_path: "/search?filter=(all mid:${id})&" +
|
|
"output=(notable:/client/summary " +
|
|
"(description citation provenance) type)&key=${key}",
|
|
|
|
// default is service_url if NULL
|
|
flyout_image_service_url: null,
|
|
|
|
flyout_image_service_path:
|
|
"/image${id}?maxwidth=75&key=${key}&errorid=/freebase/no_image_png",
|
|
|
|
// jQuery selector to specify where the flyout
|
|
// will be appended to (defaults to document.body).
|
|
flyout_parent: null,
|
|
|
|
// text snippet you want to show for the suggest
|
|
// new option
|
|
// clicking will trigger an fb-select-new event
|
|
// along with the input value
|
|
suggest_new: null,
|
|
|
|
nomatch: {
|
|
title: "No suggested matches",
|
|
heading: "Tips on getting better suggestions:",
|
|
tips: [
|
|
"Enter more or fewer characters",
|
|
"Add words related to your original search",
|
|
"Try alternate spellings",
|
|
"Check your spelling"
|
|
]
|
|
},
|
|
|
|
// CSS default class names
|
|
css: {
|
|
item_type: "fbs-item-type",
|
|
flyoutpane: "fbs-flyout-pane"
|
|
},
|
|
|
|
// the delay before sending off the ajax request to the
|
|
// suggest and flyout service
|
|
xhr_delay: 200
|
|
},
|
|
|
|
/**
|
|
* Get a value from an object multiple levels deep.
|
|
*/
|
|
get_value_by_keys: function(obj, var_args) {
|
|
var keys = $.isArray(var_args) ? var_args :
|
|
Array.prototype.slice.call(arguments, 1);
|
|
for (var i = 0; i < keys.length; i++) {
|
|
obj = obj[keys[i]];
|
|
if (obj == null) {
|
|
break;
|
|
}
|
|
}
|
|
return obj;
|
|
},
|
|
|
|
/**
|
|
* Utility method to get values of an object specified by one or more
|
|
* (nested) keys. For example:
|
|
* <code>
|
|
* get_value(my_dict, ['foo', 'bar'])
|
|
* // Would resolve to my_dict['foo']['bar'];
|
|
* </code>
|
|
* The method will return null, if any of the path specified refers to
|
|
* a null or undefined value in the object.
|
|
*
|
|
* If resolved_search_values is TRUE, this will flatten search api
|
|
* values that are arrays of entities ({mid, name})
|
|
* to an array of their names and ALWAYS return an array of strings
|
|
* of length >= 0.
|
|
*/
|
|
get_value: function(obj, path, resolve_search_values) {
|
|
if (obj == null || path == null || path.length == 0) {
|
|
return null;
|
|
}
|
|
if (!$.isArray(path)) {
|
|
path = [path];
|
|
}
|
|
var v = $.suggest.suggest.get_value_by_keys(obj, path);
|
|
if (resolve_search_values) {
|
|
if (v == null) {
|
|
return [];
|
|
}
|
|
if (!$.isArray(v)) {
|
|
v = [v];
|
|
}
|
|
var values = [];
|
|
$.each(v, function(i, value) {
|
|
if ($.type(value) === 'object') {
|
|
if (value['name'] != null) {
|
|
value = value['name'];
|
|
}
|
|
else if (value['id'] || value['mid']) {
|
|
value = value['id'] || value['mid'];
|
|
}
|
|
else if (value['value'] != null) {
|
|
// For cvts, value may contain other useful info (like date, etc.)
|
|
var cvts = [];
|
|
$.each(value, function(k, v) {
|
|
if (k !== 'value') {
|
|
cvts.push(v);
|
|
}
|
|
});
|
|
value = value['value'];
|
|
if (cvts.length) {
|
|
value += ' (' + cvts.join(', ') + ')';
|
|
}
|
|
}
|
|
}
|
|
if ($.isArray(value) && value.length) {
|
|
value = value[0].value;
|
|
}
|
|
if (value != null) {
|
|
values.push(value);
|
|
}
|
|
});
|
|
return values;
|
|
}
|
|
// Cast undefined to null.
|
|
return v == null ? null : v;
|
|
},
|
|
|
|
is_commons_id: function(id) {
|
|
if (/^\/base\//.test(id) || /^\/user\//.test(id)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Create the flyout html content given the search result
|
|
* containing output=(notable:/client/summary \
|
|
* (description citation provenance) type).
|
|
* The resulting html will be cached for optimization.
|
|
*
|
|
* @param data:Object - The search result with
|
|
* output=(notable:/client/summary \
|
|
* (description citation provenance) type)
|
|
* @param flyout_image_url:String - The url template for the image url.
|
|
* The substring, "${id}", will be replaced by data.id. It is assumed all
|
|
* parameters to the flyout image service (api key, dimensions, etc.) is
|
|
* already encoded into the url template.
|
|
*/
|
|
create_flyout: function(data, flyout_image_url) {
|
|
var get_value_by_keys = $.suggest.suggest.get_value_by_keys;
|
|
var get_value = $.suggest.suggest.get_value;
|
|
var is_system_type = $.suggest.is_system_type;
|
|
var is_commons_id = $.suggest.suggest.is_commons_id;
|
|
|
|
var name = data['name'];
|
|
var id = null;
|
|
var image = null;
|
|
var notable_props = [];
|
|
var notable_types = [];
|
|
var notable_seen = {}; // Notable types already added
|
|
var notable = get_value(data, 'notable');
|
|
if (notable && notable['name']) {
|
|
notable_types.push(notable['name']);
|
|
notable_seen[notable['name']] = true;
|
|
}
|
|
if (notable && is_system_type(notable['id'])) {
|
|
id = data['id'];
|
|
}
|
|
else {
|
|
id = data['mid'];
|
|
image = flyout_image_url.replace(/\$\{id\}/g, encodeURIComponent(id));
|
|
}
|
|
|
|
var desc_text = null;
|
|
var desc_source = null;
|
|
var desc_provider = null;
|
|
var desc_statement = null;
|
|
var descs = get_value_by_keys(
|
|
data, 'output', 'description', '/common/topic/description') || [];
|
|
if (descs.length) {
|
|
var best = descs[0];
|
|
$.each(descs, function(i, desc) {
|
|
if (get_value_by_keys(desc, 'citation', 0, 'mid') == '/m/0d07ph') {
|
|
// Prefer 'Wikipedia" descriptions (/m/0d07ph).
|
|
best = desc;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
if ($.isArray(best.value) && best.value.length) {
|
|
desc_text = best.value[0].value;
|
|
} else {
|
|
desc_text = best.value;
|
|
}
|
|
if (get_value_by_keys(best, 'provenance', 0, 'restrictions', 0) ==
|
|
'REQUIRES_CITATION') {
|
|
desc_source = get_value_by_keys(best, 'provenance', 0, 'source', 0);
|
|
desc_provider =
|
|
get_value_by_keys(best, 'citation', 'provider', 0, 'name');
|
|
if (desc_provider && $.isArray(desc_provider) &&
|
|
desc_provider.length) {
|
|
desc_provider = desc_provider[0].value;
|
|
}
|
|
desc_statement = get_value_by_keys(best, 'citation', 'statement', 0);
|
|
if (desc_statement && desc_statement.value) {
|
|
desc_statement = desc_statement.value;
|
|
}
|
|
}
|
|
} else {
|
|
// Handle "old" output description format.
|
|
$.each(['wikipedia', 'freebase'], function(i, key) {
|
|
descs = get_value(data, ['output', 'description', key], true);
|
|
if (descs && descs.length) {
|
|
desc_text = descs[0];
|
|
desc_provider = key;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
var summary = get_value(data, ['output', 'notable:/client/summary']);
|
|
if (summary) {
|
|
var notable_paths = get_value(summary, '/common/topic/notable_paths');
|
|
if (notable_paths && notable_paths.length) {
|
|
$.each(notable_paths, function(i, path) {
|
|
var values = get_value(summary, path, true);
|
|
if (values && values.length) {
|
|
values = values.slice(0, 3);
|
|
var prop_text = path.split('/').pop();
|
|
notable_props.push([prop_text, values.join(', ')]);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
var types = get_value(
|
|
data, ['output', 'type', '/type/object/type'], true);
|
|
if (types && types.length) {
|
|
$.each(types, function(i, t) {
|
|
if (!notable_seen[t]) {
|
|
notable_types.push(t);
|
|
notable_seen[t] = true;
|
|
}
|
|
});
|
|
}
|
|
var content = $('<div class="fbs-flyout-content">');
|
|
if (name) {
|
|
content.append($('<h1 id="fbs-flyout-title">').text(name));
|
|
}
|
|
content
|
|
.append($('<h3 class="fbs-topic-properties fbs-flyout-id">')
|
|
.text(id));
|
|
notable_props = notable_props.slice(0, 3);
|
|
$.each(notable_props, function(i, prop) {
|
|
content.append($('<h3 class="fbs-topic-properties">')
|
|
.append($('<strong>').text(prop[0] + ': '))
|
|
.append(document.createTextNode(prop[1])));
|
|
});
|
|
if (desc_text) {
|
|
var article = $('<p class="fbs-topic-article">');
|
|
if (desc_provider) {
|
|
if (desc_source) {
|
|
article.append($('<a class="fbs-citation">')
|
|
.attr('href', desc_source)
|
|
.attr('title', desc_statement || desc_provider)
|
|
.text('[' + desc_provider + ']'));
|
|
} else {
|
|
article.append($('<em class="fbs-citation">')
|
|
.attr('title', desc_statement || desc_provider)
|
|
.text('[' + desc_provider + '] '));
|
|
}
|
|
}
|
|
article.append(document.createTextNode(' ' + desc_text));
|
|
content.append(article);
|
|
}
|
|
if (image) {
|
|
content.children().addClass('fbs-flyout-image-true');
|
|
content.prepend(
|
|
$('<img id="fbs-topic-image" class="fbs-flyout-image-true" src="' +
|
|
image + '">'));
|
|
}
|
|
var flyout_types = $('<span class="fbs-flyout-types">')
|
|
.text(notable_types.slice(0, 3).join(', '));
|
|
var footer = $('<div class="fbs-attribution">').append(flyout_types);
|
|
|
|
return $('<div>')
|
|
.append(content)
|
|
.append(footer)
|
|
.html();
|
|
}
|
|
});
|
|
|
|
|
|
var f = document.createElement("input");
|
|
|
|
})(jQuery);
|