diff options
14 files changed, 3403 insertions, 0 deletions
diff --git a/examples/freebase-images-appengine/README b/examples/freebase-images-appengine/README new file mode 100644 index 0000000..18c53e0 --- /dev/null +++ b/examples/freebase-images-appengine/README @@ -0,0 +1,25 @@ +This is an example of a working google appengine setup. If you want to see it +in action, go to http://freebase-images.appspot.com/ + +If you are interested in incorporating the freebase-python library in your +google appengine, you should download the actual code that runs the library. +If you say: + svn checkout http://freebase-python.googlecode.com/svn/trunk/freebase freebase +in the appengine folder, you'll be able to import freebase in your appengine +code without any problems. + +In order to fully understand the example, an understanding of how appengine +works and what it does is neccessary. + +In this example, hello.py is the main program that controls four pages: +mainpage, name, recents, and info. Name takes an id and returns the name, +recents gives back the most recent images viewed (this only works if you're +using a google account), and info takes an id and returns a list of images. +Mainpage operates most of what you see. index.html the django template that +is rendered. + +The freebase queries are not all the complicated. We have a few that simply +return the name of an object given the id. The most complicated one, the one +that generates the images involves querying all the objects that are connected +to the supplied object and getting their images. + diff --git a/examples/freebase-images-appengine/app.yaml b/examples/freebase-images-appengine/app.yaml new file mode 100644 index 0000000..26936d8 --- /dev/null +++ b/examples/freebase-images-appengine/app.yaml @@ -0,0 +1,15 @@ +application: freebase-images +version: 1 +runtime: python +api_version: 1 + +handlers: +- url: /css + static_dir: stylesheets + +- url: /js + static_dir: javascript + +- url: /.* + script: hello.py + diff --git a/examples/freebase-images-appengine/hello.py b/examples/freebase-images-appengine/hello.py new file mode 100644 index 0000000..739d36f --- /dev/null +++ b/examples/freebase-images-appengine/hello.py @@ -0,0 +1,153 @@ +import cgi +import os + +from google.appengine.api import users +from google.appengine.api import urlfetch +from google.appengine.ext import webapp +from google.appengine.ext.webapp import template +from google.appengine.ext.webapp.util import run_wsgi_app +from google.appengine.ext import db + +import freebase + +class Search(db.Model): + author = db.UserProperty() + id = db.StringProperty() + url = db.StringProperty() + name = db.StringProperty() + date = db.DateTimeProperty(auto_now_add=True) + +class MainPage(webapp.RequestHandler): + def get(self): + + searches = [] + searches_avail = False + + user = users.get_current_user() + + if user: + url = users.create_logout_url(self.request.uri) + url_linktext = 'Logout' + searches = Search.gql("where author = :author order by date desc limit 5", author=user) + try: + searches[0] + searches_avail = True + except: + pass + else: + url = users.create_login_url(self.request.uri) + url_linktext = 'Login to Google' + + template_values = { + 'user' : user, + 'searches': searches, + 'searches_avail' : searches_avail, + 'url': url, + 'url_linktext': url_linktext, + } + + path = os.path.join(os.path.dirname(__file__), 'index.html') + self.response.out.write(template.render(path, template_values)) + +class Name(webapp.RequestHandler): + def get(self): + + query = { "id" : cgi.escape(self.request.get('id')), "name" : None} + r = freebase.mqlread(query) + self.response.out.write(r.name) + + +class Recents(webapp.RequestHandler): + def get(self): + user = users.get_current_user() + final = "" + + if user: + final = "<strong>Your Recent Searches:</strong> " + searches = Search.gql("where author = :author order by date desc limit 5", author=user) + bignum = -1 + for num, search in enumerate(searches): + final += "<a href='?id=" + search.id + "'>" + search.name + "</a>, " + bignum = num + + final = final[:-2] + + if bignum == -1: + final = "" + + + if not user: + final = "" + + self.response.out.write(final) + +class Info(webapp.RequestHandler): + def get(self): + VIEW = "?id=" + BASE = "http://www.freebase.com/api/trans/image_thumb" + OPTIONS = "?maxheight=300" + + id = cgi.escape(self.request.get('id')) + + if id != "": + info_query = { + "id" : id, + "name" : None, + } + + info = freebase.mqlread(info_query) + + user = users.get_current_user() + search = Search() + if user: + search.author = user + search.id = id + search.name = info.name + search.url = VIEW + id + search.put() + + + query = { + "id": id, + "/type/reflect/any_reverse": [{ + "id" : None, + "/common/topic/image": [{ + "id": None, + "optional": True, + "limit": 5 + }] + }] + } + results = freebase.mqlread(query) + images = dict() + for thing in results["/type/reflect/any_reverse"]: + imgroup = thing['/common/topic/image'] + if len(imgroup) > 0: + for image in imgroup: + images[VIEW + thing["id"]] = BASE + image['id'] + OPTIONS + + #template_values = {"images": images} + #path = os.path.join(os.path.dirname(__file__), 'images.html') + #self.response.out.write(template.render(path, template_values)) + + final = "" + for page, image in images.items(): + final += "<a href='" + page + "'><img src='" + image + "' alt='' /></a>" + if len(final) == 0: + final = "Sorry, there are no images to show." + self.response.out.write(final) + + + +application = webapp.WSGIApplication( + [('/', MainPage), + ('/info', Info), + ('/name', Name), + ('/recents', Recents)], + debug=True) + +def main(): + run_wsgi_app(application) + +if __name__ == "__main__": + main()
\ No newline at end of file diff --git a/examples/freebase-images-appengine/images.html b/examples/freebase-images-appengine/images.html new file mode 100644 index 0000000..9688822 --- /dev/null +++ b/examples/freebase-images-appengine/images.html @@ -0,0 +1,3 @@ +{% for im in images %} + <img alt="" src="{% im %}" /> +{% endfor %}
\ No newline at end of file diff --git a/examples/freebase-images-appengine/index.html b/examples/freebase-images-appengine/index.html new file mode 100644 index 0000000..95a044e --- /dev/null +++ b/examples/freebase-images-appengine/index.html @@ -0,0 +1,76 @@ +<head> + <title>Freebase Images</title> + <link type="text/css" rel="stylesheet" href="/css/freebase-controls.css"> + <link type="text/css" rel="stylesheet" href="/css/main.css"> + + <script type="text/javascript" src="http://code.jquery.com/jquery-1.2.3.min.js"></script> + <script type="text/javascript" src="/js/freebase.suggest.js"></script> + <script type="text/javascript"> + $(document).ready(function() { + $("#q").freebaseSuggest().bind("fb-select", function(e, data) { + id = data.id; + update_freebase(); + window.location.hash = "#" + data.id; + insert_images(data.id); + }); + function insert_images(id) { + $("#images").text("Loading..."); + $.get("info", {"id":id}, + function(data) { + $("#images").text(""); + $("#images").html(data); + }); + }; + function update_freebase() { + $("#onfreebase").attr("href", "http://www.freebase.com/view" + id); + $.get("name", {"id":id}, function(data){ $("#onfreebase").text(data); }); + $.get("recents", function(data) { $("#recents").html(data); }) + } + if(window.location.hash.length > 0 && $("#images").html().replace(/^\s+|\s+$/g, "").length == 0) { + id = window.location.hash.split("#")[1]; + update_freebase(); + insert_images(id); + } + else if(window.location.search.length > 0 && $("#images").html().replace(/^\s+|\s+$/g, "").length == 0) { + id = window.location.search.split("?id=")[1] + update_freebase(); + insert_images(id); + + } + }); + </script> +</head> + +<html> + <body> + <div id="input"> + <form> + <h1>Search: <input type="text" id="q" /></h1> + <div id="recents"> + {% if user %} + {% if searches_avail %} + <strong>Your Recent Searches:</strong> + {% for search in searches %} + <a href="?id={{ search.id }}">{{ search.name }}</a>{% if not forloop.last %},{% endif %} + {% endfor %} + {% endif %} + {% else %} + <a href="{{ url }}">Login to Google</a> to keep track of your recent searches. + {% endif %} + </div> + </form> + </div> + + <div id="freebase"> + Get more information about <a id="onfreebase" href="http://www.freebase.com/view/">everything</a> on Freebase! + </div> + + <div id="images"> + + </div> + + <div id="footer"> + Data and images from <a href="http://www.freebase.com">Freebase</a> | <a id="login" href="{{ url }}">{{ url_linktext }}</a> + </div> + </body> +</html>
\ No newline at end of file diff --git a/examples/freebase-images-appengine/index.yaml b/examples/freebase-images-appengine/index.yaml new file mode 100644 index 0000000..c247d55 --- /dev/null +++ b/examples/freebase-images-appengine/index.yaml @@ -0,0 +1,18 @@ +indexes: + +# AUTOGENERATED + +# This index.yaml is automatically updated whenever the dev_appserver +# detects that a new type of query is run. If you want to manage the +# index.yaml file manually, remove the above marker line (the line +# saying "# AUTOGENERATED"). If you want to manage some indexes +# manually, move them above the marker line. The index.yaml file is +# automatically uploaded to the admin console when you next deploy +# your application using appcfg.py. + +# Used 16 times in query history. +- kind: Search + properties: + - name: author + - name: date + direction: desc diff --git a/examples/freebase-images-appengine/javascript/freebase.select.js b/examples/freebase-images-appengine/javascript/freebase.select.js new file mode 100644 index 0000000..bd94d2b --- /dev/null +++ b/examples/freebase-images-appengine/javascript/freebase.select.js @@ -0,0 +1,1280 @@ +/****************************************************************************** + * All source and examples in this project are subject to the + * following copyright, unless specifically stated otherwise + * in the file itself: + * + * Copyright (c) 2007, Metaweb Technologies, 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. + * + * THIS SOFTWARE IS PROVIDED BY METAWEB TECHNOLOGIES ``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 METAWEB TECHNOLOGIES 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. + *****************************************************************************/ + + +// Use existing freebase namespace +// if it exists +if (!window.freebase) + window.freebase = {}; + +if (!window.freebase.controls) + window.freebase.controls = {}; + +(function($, fb) { + +/** + * !!! NOTICE: This requires jQuery rev 3578 or higher !!! + * + * Version 0.4 + */ + + +/** + * Apply the specified input control behavior to an input with the specified options + * + * @param control:InputControl - @see InputControl + * @param options:Object - dictionary of options that overwrite control.default_options + */ +$.fn._freebaseInput = function(control, options) { + if (!options) options = {}; + return this + .attr("autocomplete", "off") + .each(function() { + control.release(this); + var owner = this; + $.each(["focus", "click"], function(i,n) { + $(owner).unbind(n, control.delegate(n)).bind(n, control.delegate(n)); + }); + + // we might be just resetting the options + if (typeof this['fb_id'] == 'undefined') + this.fb_id = control.counter++; + // flush cache + control.cache[this.fb_id] = null; + // store options in hash + var o = {}; + $.extend(o, control.default_options, options); + control.option_hash[this.fb_id] = o; + + // If initialize option is true then start off control as managed. + if (options.initialize) { + control.manage(this); + } + }); +}; + +/** + * use firebug's console if it exists + */ +fb.log = fb.error = fb.debug = function() {}; +if (typeof console != "undefined" && console.log && console.error) { + fb.log = console.log; + fb.error = console.error; + fb.debug = console.debug; +}; + + +/** + * a function wrapper to be invoked within a context object (thisArg) with + * additional parameters (argArray). + * + * @param fn:Function - Function to run. + * @param thisArg:Object - Context in which to run the function (thisArg) + * @param argArray:Array - extra arguments to be appended to func's own arguments. + * So if a callback invokes func with an eventObject, func will be called + * with: func(arg1, arg2,..., argN, eventObject) + */ +fb.delegate = function(fn, thisArg, argArray) { + if (typeof argArray == "undefined") + argArray = []; + var dg = function(){ + // 'arguments' isn't technically an array, so we can't just use concat + var f_args = []; + for(var i=0, len=arguments.length; i<len; i++) + f_args.push(arguments[i]); + if (arguments.callee && arguments.callee.fn) + return (arguments.callee.fn.apply(arguments.callee.thisArg, arguments.callee.argArray.concat(f_args))); + return undefined; + }; + + dg.thisArg = thisArg; + dg.fn = fn; + dg.argArray = argArray; + + return (dg); +}; + +fb.quote_id = function(id) { + if (id.charAt(0) == '/') + return id; + else + return ('/' + encodeURIComponent(id)); +}; + + +/** + * simple state object + */ +fb.state = function() {}; +fb.state.prototype = { + enter: function(data) {}, + exit: function(data) {}, + handle: function(data) {} +}; + +/** + * simple state machine + */ +fb.state_machine = function(states) { + // states: [["STATE_NAME_1", state_1],...,["STATE_NAME_n", state_n]] + this.current_state = null; + this.states = {}; + var owner = this; + $.each(states, function(i,n) { + n[1].sm = owner; + owner.states[n[0]] = n[1]; + if (i==0) + owner.current_state = n[0]; + }); + if (!this.current_state) + throw "StateMachine must be initialized with at least one state"; + this.states[this.current_state].enter(); +}; +fb.state_machine.prototype = { + transition: function(to_state, exit_data, enter_data, data) { //fb.log("state_machine.transition current_state: ", this.current_state, "to_state: ", to_state); + // to_state: the target destination state + // exit_data: the exit data for current state + // enter_data: the enter data for to_state + // data: the data for to_state.handle + var target = this.states[to_state]; + if (!target) + throw("Unrecongized state:" + to_state); + + var source = this.states[this.current_state]; + + // exit current state + source.exit(exit_data); + + // enter target state + target.enter(enter_data); + + this.current_state = to_state; + + // handle data + this.handle(data); + }, + handle: function(data) { + if (data) + this.states[this.current_state].handle(data); + } +}; + + +/** + * InputControl class + */ +fb.InputControl = function() { + this.default_options = {}; + this.counter = 0; + this.cache = {}; + this.option_hash = {}; + this.sm = null; + this.delegates = {}; + this.dropdown_delay = 30; + this.manage_delay = 20; + this.release_delay = 10; +}; + +fb.InputControl.prototype = { + /** + * facility to reuse delegate functions with different arguments + */ + delegate: function(fname, argArray) { + if (!this.delegates[fname]) + this.delegates[fname] = fb.delegate(this[fname], this); + this.delegates[fname].argArray = argArray ? argArray : []; + return this.delegates[fname]; + }, + + options: function(input) {//fb.log("this.options", input); + var o = this.option_hash[input.fb_id]; + if (!o) + throw "Unknown input"; + return o; + }, + + transition: function(state) { + if (this.sm) + this.sm.transition(state); + }, + + handle: function(data) { + if (this.sm) + this.sm.handle(data); + }, + + /** + * get input value, if null, return empty string ("") + */ + val: function(input) { + var v = $(input).val(); + if (v == null) + return ""; + return $.trim(v); + }, + + /** + * get "name" or "text" field of an object. if none return "unknown" + */ + name: function(obj) { + // backwards compatibility with data.text and data.name + if (obj.text != null) + return obj.text; + if (obj.name != null) + return obj.name; + return "unknown"; + }, + + /** + * text change delay variable to length of string + */ + delay: function(l) { + var t = .3; + if (l > 0) + t = 1/(6 * (l-0.7)) + .3; + return t * 1000; + }, + + manage: function(input) { + this.release(input); + var owner = this; + $.each(["blur", "keydown", "keypress", "keyup", "input", "paste"], function(i,n) { + $(input).bind(n, owner.delegate(n)); + }); + this.transition("start"); + //this.handle({id:"TEXTCHANGE", input:input}); + this.manage_hook(input); + }, + + // over-ride to handle manage + manage_hook: function(input) {}, + + release: function(input) {//fb.log("release", input); + var owner = this; + $.each(["blur", "keydown", "keypress", "keyup", "input", "paste"], function(i,n) { + $(input).unbind(n, owner.delegate(n)); + }); + + this.transition("start"); + this.release_hook(input); + }, + + // over-ride to handle release + release_hook: function(input) {}, + + destroy: function(input) { + //Clear all timeouts + window.clearTimeout(this.manage_timeout); + window.clearTimeout(this.release_timeout); + window.clearTimeout(this.textchange_timeout); + window.clearTimeout(this.loadmsg_timeout); + + //Clear all event binding + var owner = this; + $.each(["focus", "click"], function(i,n) { + $(input).unbind(n, owner.delegate(n)); + }); + this.release(input); + }, + + focus: function(e) {//fb.log("on_focus", e); + window.clearTimeout(this.manage_timeout); + var input = e.target; + try { + this.options(input); + } + catch(e) { + return; + } + this.manage_timeout = window.setTimeout(this.delegate("manage", [input]), this.manage_delay); + }, + + blur: function(e) {//fb.log("on_blur", e.target, this, this.dont_release, this._input); + window.clearTimeout(this.release_timeout); + var input = $(e.target)[0]; + if (this.dont_release) { + // the current input we are losing focus on + // because we've clicked on the list/listitem + this._input = input; + return; + } + this.release_timeout = window.setTimeout(this.delegate("release", [input]), this.release_delay); + }, + + keydown: function(e) {//fb.log("on_keydown", e.keyCode); + switch(e.keyCode) { + case 9: // tab + this.tab(e); + break; + case 38: // up + case 40: // down + // prevents cursor/caret from moving (in Safari) + e.preventDefault(); + break; + default: + break; + } + }, + + keypress: function(e) {//fb.log("on_keypress", e.keyCode); + switch(e.keyCode) { + case 38: // up + case 40: // down + // prevents cursor/caret from moving + if (!e.shiftKey) + e.preventDefault(); + break; + case 13: // return + this.enterkey(e); + break ; + case 27: // escape + this.escapekey(e); + break; + default: + break; + } + }, + + keyup: function(e) {//fb.log("on_keyup", e.keyCode); + switch(e.keyCode) { + case 38: // up + e.preventDefault(); + this.uparrow(e); + break; + case 40: // down + e.preventDefault(); + this.downarrow(e); + break; + case 9: // tab + case 13: // enter + case 16: // ctrl + case 17: // shift + case 18: // option/alt + case 27: // escape + case 37: // left + case 39: // right + case 224:// apple/command + break; + default: + this.textchange(e); + break; + } + }, + + click: function(e) { + //Override this to handle mouseclicks + }, + + // Mozilla only, to detech paste + input: function(e) {//fb.log("on_input", e); + this.textchange(e); + }, + + // IE only, to detect paste + paste: function(e) {//fb.log("on_paste", e); + this.textchange(e); + }, + + uparrow: function(e) { + this.handle({id:"UPARROW", input:e.target}); + }, + + downarrow: function(e) { + this.handle({id:"DOWNARROW", input:e.target}); + }, + + tab: function(e) { + this.handle({id:"TAB", input:e.target, domEvent:e}); + }, + + enterkey: function(e) { + if(e.shiftKey) { + this.handle({id:"ENTERKEY-SHIFT", input:e.target, domEvent:e}); + } else { + this.handle({id:"ENTERKEY", input:e.target, domEvent:e}); + } + }, + + escapekey: function(e) { + this.handle({id:"ESCAPEKEY", input:e.target}); + }, + + textchange: function(e) {//fb.log("on_textchange", e.target); + window.clearTimeout(this.textchange_timeout); + // Save inputted text + this.input_text = this.val(e.target); + var delay = this.delay(this.input_text.length); + this.textchange_timeout = window.setTimeout(this.delegate("textchange_delay", [e.target]), delay); + }, + + textchange_delay: function(input){//fb.log("on_textchange_delay", input); + this.handle({id:"TEXTCHANGE", input:input}); + } +}; + +/** + * InputSelectControl class + * superClass: InputControl + */ +fb.InputSelectControl = function() { + fb.InputControl.call(this); + this.min_len = 1; + this.should_filter = true; + this.fudge = 8; + this.loadmsg_delay = 50; + + /** + * initialize the select state machine + * + * states: + * start: + * getting: + * selecting: + */ + this.sm = new fb.state_machine([ + ["start", new state_start(this)], + ["getting", new state_getting(this)], + ["selecting", new state_selecting(this)] + ]); +}; +// inheritance: prototype/constructor chaining +fb.InputSelectControl.prototype = new fb.InputControl(); +fb.InputSelectControl.prototype.constructor = fb.InputSelectControl; + +// shorthand for fb.InputSelectControl.prototype +var p = fb.InputSelectControl.prototype; + +p.get_list = function(){ + return $("#fbs_list > .fbs-bottomshadow > .fbs-ul")[0]; +}; + +p.get_list_items = function(){ + return $(this.get_list()).children("li"); +}; + +p.release_hook = function(input) { + this.list_hide(); +}; + +p.click_listitem = function(li) {//fb.log("click_listitem", li, this._input); + this.handle({id:"LISTITEM_CLICK", item:li, input:this._input}); +}; + +p.mousedown_list = function(e) {//fb.log("mousedown_list", e, this); + // hack in IE/safari to keep suggestion list from disappearing when click/scrolling + this.dont_release = true; +}; + +p.mouseup_list = function(e) {//fb.log("mouseup_list", e, this, this._input); + // hack in IE/safari to keep suggestion list from disappearing when click/scrolling + if (this._input) { + $(this._input).unbind("focus", this.delegate("focus")); + $(this._input).focus(); + window.setTimeout(this.delegate("reset_focus", [this._input]), 0); + //$(this._input).focus(this.delegate("focus")); + } + this.dont_release = false; +}; + +p.reset_focus = function(input) { + $(input).focus(this.delegate("focus")); +}; + +p.list_load = function(input) {//fb.log("list_load", input); + throw "You must override InputSelectControl.prototype.list_load"; +}; + +p.list_receive = function(input, txt, o) {//fb.log("list_receive", input, query, o); + // handle errors + if (o.status !== '200 OK') { + fb.error("list_receive", o.code, o.messages, o); + return; + } + + // currently, list_receive recognizes results of the forms: + // 1. { list: { listItems: [...] } } + // 2. { results: [...] } + // 3. { result: [...] } + // 4. { query: { result: [...] } } + var result = []; + if ("list" in o && "listItems" in o.list) + result = o.list.listItems; + else if ("result" in o) + result = o.result; + else if ("results" in o) + result = o.results; + else if ("query" in o && "result" in o.query) + result = o.query.result; + else { + fb.error("list_receive", o.code, "Unrecognized list result", o); + return; + } + + // hook to update cache + this.list_receive_hook(input, txt, result); + + // handle result + this.handle({id:"LIST_RESULT", input:input, result:result}); +}; + +p.list_receive_hook = function(input, txt, result) { + // overwrite to process search result + // like updating the cache +}; + +p.position = function(element, input) { + var options = this.options(input); + var pos = $(input).offset({border: true, padding: true}); + var left = pos.left; + var top = pos.top + input.clientHeight + this.fudge; + + var right = pos.left + options.width; + var window_right = $(window).width() + document.body.scrollLeft; + + // If the right edge of the dropdown extends beyond the right + // of the screen then right-justify the dropdown to the input. + if (right > window_right && options.width > $(input).outerWidth()) { + left = left - (options.width - $(input).outerWidth()) + 4; + } + $(element) + .css({top:top, left:left, width:options.width}) + .show(); +} + +p.list_show = function(input, result) {//fb.log("list_show", input, result); + if (!input) + return; + if (!result) + result = []; + var options = this.options(input); + var txt = this.val(input); + var list = null; + if (!$("#fbs_list").length) { + $(document.body) + .append( + '<div style="display:none;position:absolute" id="fbs_list" class="fbs-topshadow">' + + '<div class="fbs-bottomshadow">'+ + '<ul class="fbs-ul"></ul>' + + '</div>' + + '</div>'); + + list = $("> .fbs-ul")[0]; + } + if (!list) + list = this.get_list(); + + $("#fbs_list > .fbs-bottomshadow") + .unbind() + .mousedown(this.delegate("mousedown_list")) + .mouseup(this.delegate("mouseup_list")) + .scroll(this.delegate("mousedown_list")); + + + // unbind all li events and empty list + $("li", list) + .each(function() { + $(this).unbind(); + this.fb_data = null; // clean up expando variable "fb_data" + }); + $(list).empty(); + + var filter = this.filter; + if (typeof options.filter == "function") + filter = options.filter; + + if (!result.length) + $(list).append(this.create_list_item({id:"NO_MATCHES", text:"no matches"}, null, options).addClass("fbs-li-nomatch")); + + var filtered = []; + if(this.should_filter) { + $.each(result, function(i, n) { + if (filter.apply(null, [n, txt])) + filtered.push(n); + }); + filtered = this.filter_hook(filtered, result); + } else { + filtered = result; + } + var owner = this; + $.each(filtered, function(i, n) { + $(list).append(owner.create_list_item(n, txt, options, i)); + }); + + // hook to add additional html elemments and handlers + // like "Create New" item under the list + this.list_show_hook(list, input, options); + + this.position($("#fbs_list"), input); +}; + +p.list_show_hook = function(list, input, options) { }; + +p.filter_hook = function(filtered, result) { + return filtered; +}; + +p.list_hide = function() {//fb.log("list_hide"); + $("#fbs_list").hide(); + this.list_hide_hook(); +}; + +p.list_hide_hook = function() {}; + +p.create_list_item = function(data, txt, options, index) { + var li = $("<li class='fbs-li'></li>")[0]; + + var trans = this.transform; + if (typeof options.transform == "function") + trans = options.transform; + + var html = trans.apply(this, [data, txt]); + + $(li).append(html); + + data.index = index; + + // sometimes data contains text and/or name + if ("text" in data) + data.name = data.text; + + li.fb_data = data; + + var owner = this; + return $(li) + .mouseover(function(e) { + owner.list_select(null, this, options); + }) + .click(function(e) { + owner.click_listitem(this); + }); +}; + +/** + * The default filter + * + * @param data - The individual item from the ac_path service. + * @return TRUE to include in list or FALSE to exclude from list. + */ +p.filter = function(data, txt) { + return true; +}; + +/** + * The default transform + * + * @param data - The individual item from the ac_path service + * @param txt - The input string + * @param options - Options used for the input + * @return a DOM element or html that will be appended to an <li/> + */ +p.transform = function(data, txt) { + return data; +}; + +/** + * show loading message + */ +p.loading_show = function(input) { + this.list_hide(); + if (!$("#fbs_loading").length) { + $(document.body) + .append( + '<div style="display:none;position:absolute" id="fbs_loading" class="fbs-topshadow">' + + '<div class="fbs-bottomshadow">'+ + '<ul class="fbs-ul">' + + '<li class="fbs-li">'+ + '<div class="fbs-li-name">loading...</div>' + + '</li>' + + '</ul>' + + '</div>' + + '</div>'); + } + this.position($("#fbs_loading"), input); +}; + +/** + * hide loading message + */ +p.loading_hide = function() { + $("#fbs_loading").hide(); +}; + +p.list_select = function(index, li, options) { + var sli = null; + this.get_list_items().each(function(i,n) { + if (i == index || li == n) { + $(n).addClass("fbs-li-selected"); + sli = n; + } + else + $(n).removeClass("fbs-li-selected"); + }); + this.list_select_hook(sli, options); + return sli; +}; + +/** + * list select hook + * @param sli - list item (li) html element + */ +p.list_select_hook = function(sli, options) { }; + +p.list_length = function() { + return this.get_list_items().length; +}; + +p.list_selection = function(returnObj) { + if (!returnObj) + returnObj = {}; + returnObj.index = -1; + returnObj.item = null; + this.get_list_items().each(function(i,n){ + if (n.className.indexOf("fbs-li-selected") != -1) { + returnObj.index = i; + returnObj.item = n; + return false; + } + }); + return returnObj; +} + +p.list_select_next = function(options, data) { + var len = this.list_length(); + var obj = this.list_selection(); + var index = obj.index+1; + if (index >=0 && index < len){ + var sel = this.list_select(index, null, options); + // Change input value to reflect selected item + var txt = $(".fbs-li-name", sel).text(); + $(data.input).val(txt); + return sel + } else if (options.soft) { + // Since no item is currently selected, + // change input value to reflect previously inputted text + $(data.input).val(this.input_text); + return this.list_select(null, null, options); + } else if (len > 0) { + return this.list_select(0, null, options); + } + return null; +}; + +p.list_select_prev = function(options, data) { + var len = this.list_length(); + var obj = this.list_selection(); + var index = obj.index-1; + if (index >=0 && index < len) { + var sel = this.list_select(index, null, options); + // Change input value to reflect selected item + var txt = $(".fbs-li-name", sel).text(); + $(data.input).val(txt); + return sel + } else if (options.soft) { + if (index < -1 && len > 0) { + return this.list_select(len - 1, null, options); + } else { + // Since no item is currently selected, + // change input value to reflect previously inputted text + $(data.input).val(this.input_text); + return this.list_select(null, null, options); + } + } else if (len > 0) + return this.list_select(len - 1, null, options); + return null; +}; + +p.scroll_into_view = function(elt, p) { + if (!elt) + return; + if (!p) + p = elt.parentNode; + if (!p) { + elt.scrollIntoView(false); + return; + } + + if (elt.offsetTop < p.scrollTop) { + p.scrollTop = elt.offsetTop; + return; + } + + var elt_h = $(elt).height(); + var p_h = $(p).height(); + if ((elt.offsetTop + elt_h) > (p.scrollTop + p_h)) { + p.scrollTop = elt.offsetTop + elt_h - p_h; + } +}; + +/** + * emphasize part of the html text with <em/> + */ +p.em_text = function(text, em_str) { + var em = text; + var index = text.toLowerCase().indexOf(em_str.toLowerCase()); + if (index >= 0) { + em = text.substring(0, index) + + '<em class="fbs-em">' + + text.substring(index, index+em_str.length) + + '</em>' + + text.substring(index + em_str.length); + } + return em; +}; + +p.caret_last = function(input) { + var l = this.val(input).length; + if (input.createTextRange) { + // IE + var range = input.createTextRange();; + range.collapse(true); + range.moveEnd("character", l); + range.moveStart("character", l); + range.select(); + } + else if (input.setSelectionRange) { + // mozilla + input.setSelectionRange(l, l); + } +}; + +/** + * base class for all select states + * @param c:InputSelectControl + */ +function select_state(c) { + fb.state.call(this); + this.c = c; +}; +// inheritance: prototype/constructor chaining +select_state.prototype = new fb.state(); +select_state.prototype.constructor = select_state; + +/** + * state: start + */ +function state_start(c) { + select_state.call(this, c); +}; +// inheritance: prototype/constructor chaining +state_start.prototype = new select_state(); +state_start.prototype.constructor = state_start; + +state_start.prototype.handle = function(data) {//fb.log("state_start.handle", data); + if (!data || !data.input) + return; + var options = this.c.options(data.input); + switch (data.id) { + case "TEXTCHANGE": + case "DOWNARROW": + var txt = this.c.val(data.input); + if (txt.length >= this.c.min_len) + this.sm.transition("getting", null, data); + else + this.c.list_hide(); + break; + case "DROPDOWN": + this.sm.transition("getting", null, data); + break; + case "ENTERKEY": + $(data.input).trigger("fb-noselect", [data]); + this.sm.transition("start"); + window.clearTimeout(this.c.textchange_timeout); + break; + case "ENTERKEY-SHIFT": + data.domEvent.preventDefault(); + break; + default: + break; + }; +}; + +/** + * state: getting + */ +function state_getting(c) { + select_state.call(this, c); +}; +// inheritance: prototype/constructor chaining +state_getting.prototype = new select_state(); +state_getting.prototype.constructor = state_getting; + +state_getting.prototype.enter = function(data) {//fb.log("state_getting.enter", data); + window.clearTimeout(this.c.loadmsg_timeout); + if (!data || !data.input) + return; + // show loading msg + this.c.loadmsg_timeout = window.setTimeout(this.c.delegate("loading_show", [data.input]), this.c.loadmsg_delay); + // request autocomplete url + this.c.list_load(data.input); +}; +state_getting.prototype.exit = function(data) {//fb.log("state_getting.exit", data); + // hide loading msg + window.clearTimeout(this.c.loadmsg_timeout); + this.c.loading_hide(); +}; +state_getting.prototype.handle = function(data) {//fb.log("state_getting.handle", data); + if (!data || !data.input) + return; + var options = this.c.options(data.input); + switch (data.id) { + case "TEXTCHANGE": + this.sm.transition("start", null, null, data); + break; + case "LIST_RESULT": + this.sm.transition("selecting", null, data); + break; + case "ENTERKEY": + $(data.input).trigger("fb-noselect", [data]); + this.c.list_hide(); + this.sm.transition("start"); + window.clearTimeout(this.c.textchange_timeout); + break; + case "ENTERKEY-SHIFT": + data.domEvent.preventDefault(); + break; + case "ESCAPEKEY": + this.c.list_hide(); + this.sm.transition("start"); + break; + default: + break; + }; +}; + +/** + * state: selecting + */ +function state_selecting(c) { + select_state.call(this, c); +}; +// inheritance: prototype/constructor chaining +state_selecting.prototype = new select_state(); +state_selecting.prototype.constructor = state_selecting; + +state_selecting.prototype.enter = function(data) {//fb.log("state_selecting.enter", data); + if (!data || !data.input || !data.result) + return; + this.c.list_show(data.input, data.result); + var options = this.c.options(data.input); + if (!options.soft) + this.c.list_select(0, null, options); +}; +state_selecting.prototype.exit = function(data) {//fb.log("state_selecting.exit", data); + this.c.list_select(null); +}; +state_selecting.prototype.handle = function(data) {//fb.log("state_selecting.handle", data); + if (!data || !data.input) + return; + var options = this.c.options(data.input); + switch (data.id) { + case "TEXTCHANGE": + this.c.should_filter = true; + this.sm.transition("start", null, null, data); + break; + case "DOWNARROW": + $("#fbs_list").show(); + var li = this.c.list_select_next(options, data); + this.c.scroll_into_view(li); + break; + case "UPARROW": + $("#fbs_list").show(); + var li = this.c.list_select_prev(options, data); + this.c.scroll_into_view(li); + break; + case "DROPDOWN": + if(this.c.val(data.input) == "") { + this.sm.transition("start"); + this.c.list_hide(); + } else { + this.sm.transition("getting", null, data); + this.c.should_filter = !this.c.should_filter; + } + break; + case "TAB": + var s = this.c.list_selection(); + if (s.index == -1 || !s.item) { + $(data.input).trigger("fb-noselect", [data]); + return; + } + this.listitem_select(data.input, s.item); + break; + case "ENTERKEY-SHIFT": + // Create new topic if user is holding shift while hitting enter + this.c.create_new(data.input); + data.domEvent.preventDefault(); + break; + case "ENTERKEY": + var s = this.c.list_selection(); + if (s.index == -1 || !s.item) { + $(data.input).trigger("fb-noselect", [data]); + return; + } + if ($("#fbs_list").css("display") != "none"){ + data.domEvent.preventDefault(); + } else { + $(data.input).trigger("fb-submit", [s.item.fb_data]); + return; + } + this.listitem_select(data.input, s.item); + break; + case "LISTITEM_CLICK": + this.listitem_select(data.input, data.item); + break; + case "ESCAPEKEY": + this.c.list_hide(); + this.sm.transition("start"); + break; + default: + break; + }; +}; + +state_selecting.prototype.listitem_select = function(input, item) { + if (!item) + return; + switch(item.fb_data.id) { + case "NO_MATCHES": + break; + default: + var txt = $(".fbs-li-name", item).text(); + $(input).val(txt); + this.c.caret_last(input); + $(input).trigger("fb-select", [item.fb_data]) + .trigger("suggest", [item.fb_data]); // legacy - for compatibility + this.c.list_hide(); + break; + } +}; + + +function use_jsonp(options) { + // if we're on the same host, then we don't need to use jsonp. This + // greatly increases our cachability + if (!options.service_url) + return false; // no host == same host == no jsonp + var pathname_len = window.location.pathname.length; + var hostname = window.location.href; + var hostname = hostname.substr(0, hostname.length - pathname_len); + //console.log("Hostname = ", hostname); + if (hostname == options.service_url) + return false; + + return true; +} +/** + * freebaseSelect() allows one to select from an enumeration of freebase topics + * of a certain freebase type under an input box. + * + * freebaseSelect accepts a single argument which is an options Object with + * the following attributes: + * + * width: This is the width of the suggestion list and the flyout in + * pixels. Default is 275. + * + * service_url: This the base url to all the api services like autocomplete, + * blurbs and thumbnails. Default is "http://www.freebase.com". + * + * mqlread_path: The path to the mql service. Default is "/api/service/mqlread". + * + * type: The freebase type id to enumerate on. Default is "/location/us_state". + * + * limit: The max number of instances to show. + * + * initialize: Set to true for control to be managed from the start. Use when you know + * that the control started with the focus. (Default: false) + * + * In addition, freebaseSelect will trigger the following events on behalf of + * the input it's attached to. They include: + * + * fb-select: Triggered when something is selected from the selection + * list. The data object will contain id and name fields: + * { id: aString, name: aString }. + * + * @example + * $('#myInput') + * .freebaseSelect({type:"/location/us_state", limit: 50}) * + * .bind('fb-select', function(e, data) { console.log('select: ', data.id); }) + * + * @desc Choose from an enumeration of all the 50 States. + * + * @name freebaseSelect + * @param options object literal containing options + * @return jQuery + * @cat Plugins/Freebase + * @type jQuery + */ +$.fn.freebaseSelect = function(action, options) { + if (typeof action == 'object' && options == null) { + // Only passed options so assume activation + options = action; + action = "activate"; + } + + if (action == 'activate') { + $(this).addClass("fbs-enumcontrol"); + $(this)._freebaseInput(fb.select.getInstance(), options); + } else if (action == 'destroy') { + $(this).removeClass("fbs-enumcontrol"); + fb.select.getInstance().destroy(this); + } + + return $(this); +}; + +function SelectControl() { + fb.InputSelectControl.call(this); + this.default_options = { + width: 275, // width of list and flyout + service_url: "http://www.freebase.com", + mqlread_path: "/api/service/mqlread", + type: "/location/us_state", + soft: true, + limit: 100, + filter: null, + transform: null, + initialize: false + }; + this.min_len = 0; +}; +// inheritance: prototype/constructor chaining +SelectControl.prototype = new fb.InputSelectControl(); +SelectControl.prototype.constructor = SelectControl; + +SelectControl.instance = null; +SelectControl.getInstance = function() { + if (!SelectControl.instance) + SelectControl.instance = new SelectControl(); + return SelectControl.instance; +}; + +// shorthand for SelectControl.prototype +var p = SelectControl.prototype; + +p.list_load = function(input) {//fb.log("list_load", input); + if (!input) + return; + if (!"fb_id" in input) + return; + var txt = this.val(input); + if (this.cache[input.fb_id]) { + //fb.log("load from cache: ", this.cache[input.fb_id]); + window.clearTimeout(this.handle_timeout); + this.handle_timeout = window.setTimeout(this.delegate("handle", [{id:"LIST_RESULT", input:input, result:this.cache[input.fb_id]}]), 0); + return; + } + var options = this.options(input); + // mql read + var param = {} + if (options.mql_query) + param.queries = + '{' + + '"query": {' + + '"query": ' + options.mql_query + + '}' + + '}'; + + else { + var q = '{' + + '"query":{' + + '"query":[{' + + '"name":null,' + + '"id":null,' + + '"sort":"name",' + + '"type":"'+options.type+'",'+ + '"limit":'+options.limit + + '}]' + + '}' + + '}'; + param.queries = q; + } + $.ajax({ + type: "GET", + url: options.service_url + options.mqlread_path, + data: param, + success: this.delegate("list_receive", [input, txt]), + dataType: use_jsonp(options) ? "jsonp" : "json", + cache: true + }); +}; + +p.list_receive_hook = function(input, txt, result) { + // update cache + this.cache[input.fb_id] = result; +}; + +p.list_show_hook = function(list, input, options) { + $(list).next(".fbs-selectnew").hide(); + + var text = this.val(input); + if(text && text.length > 0) { + var li = $("li.fbs-li:contains("+text+"):first", list); + if(li.length > 0) + this.scroll_into_view(li[0]); + } +}; + +p.transform = function(data, txt) { + var div = document.createElement("div"); + $(div).append('<div class="fbs-li-name"></div>'); + var text = $(".fbs-li-name", div).append(document.createTextNode(data.name)).text(); + return div.innerHTML; +}; + +p.filter = function(data, txt) { + if (txt == "") + return true; + return data.name && data.name.toLowerCase().indexOf(txt.toLowerCase()) != -1; +}; + +p.filter_hook = function(filtered, result) { + if (!filtered.length) + return result; + return filtered; +}; + +p.create_new = function(input){ + //Can't create new topics with select control + return; +}; + +p.click = function(e) { + var x = e.clientX - $(e.target).offset().left; + var width = e.target.clientWidth - 10; + if(x > width) { + //Delay handling of dropdown so that it occurs after manage_delay + window.setTimeout(this.delegate("handle", [{id:"DROPDOWN", input:e.target}]), this.dropdown_delay); + } +}; + +fb.select = SelectControl; + + +})(jQuery, window.freebase.controls); diff --git a/examples/freebase-images-appengine/javascript/freebase.suggest.js b/examples/freebase-images-appengine/javascript/freebase.suggest.js new file mode 100644 index 0000000..215fa53 --- /dev/null +++ b/examples/freebase-images-appengine/javascript/freebase.suggest.js @@ -0,0 +1,1604 @@ +/****************************************************************************** + * All source and examples in this project are subject to the + * following copyright, unless specifically stated otherwise + * in the file itself: + * + * Copyright (c) 2007, Metaweb Technologies, 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. + * + * THIS SOFTWARE IS PROVIDED BY METAWEB TECHNOLOGIES ``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 METAWEB TECHNOLOGIES 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. + *****************************************************************************/ + + +// Use existing freebase namespace +// if it exists +if (!window.freebase) + window.freebase = {}; + +if (!window.freebase.controls) + window.freebase.controls = {}; + +(function($, fb) { + +/** + * !!! NOTICE: This requires jQuery rev 3578 or higher !!! + * + * Version 0.4 + */ + + +/** + * Apply the specified input control behavior to an input with the specified options + * + * @param control:InputControl - @see InputControl + * @param options:Object - dictionary of options that overwrite control.default_options + */ +$.fn._freebaseInput = function(control, options) { + if (!options) options = {}; + return this + .attr("autocomplete", "off") + .each(function() { + control.release(this); + var owner = this; + $.each(["focus", "click"], function(i,n) { + $(owner).unbind(n, control.delegate(n)).bind(n, control.delegate(n)); + }); + + // we might be just resetting the options + if (typeof this['fb_id'] == 'undefined') + this.fb_id = control.counter++; + // flush cache + control.cache[this.fb_id] = null; + // store options in hash + var o = {}; + $.extend(o, control.default_options, options); + control.option_hash[this.fb_id] = o; + + // If initialize option is true then start off control as managed. + if (options.initialize) { + control.manage(this); + } + }); +}; + +/** + * use firebug's console if it exists + */ +fb.log = fb.error = fb.debug = function() {}; +if (typeof console != "undefined" && console.log && console.error) { + fb.log = console.log; + fb.error = console.error; + fb.debug = console.debug; +}; + + +/** + * a function wrapper to be invoked within a context object (thisArg) with + * additional parameters (argArray). + * + * @param fn:Function - Function to run. + * @param thisArg:Object - Context in which to run the function (thisArg) + * @param argArray:Array - extra arguments to be appended to func's own arguments. + * So if a callback invokes func with an eventObject, func will be called + * with: func(arg1, arg2,..., argN, eventObject) + */ +fb.delegate = function(fn, thisArg, argArray) { + if (typeof argArray == "undefined") + argArray = []; + var dg = function(){ + // 'arguments' isn't technically an array, so we can't just use concat + var f_args = []; + for(var i=0, len=arguments.length; i<len; i++) + f_args.push(arguments[i]); + if (arguments.callee && arguments.callee.fn) + return (arguments.callee.fn.apply(arguments.callee.thisArg, arguments.callee.argArray.concat(f_args))); + return undefined; + }; + + dg.thisArg = thisArg; + dg.fn = fn; + dg.argArray = argArray; + + return (dg); +}; + +fb.quote_id = function(id) { + if (id.charAt(0) == '/') + return id; + else + return ('/' + encodeURIComponent(id)); +}; + + +/** + * simple state object + */ +fb.state = function() {}; +fb.state.prototype = { + enter: function(data) {}, + exit: function(data) {}, + handle: function(data) {} +}; + +/** + * simple state machine + */ +fb.state_machine = function(states) { + // states: [["STATE_NAME_1", state_1],...,["STATE_NAME_n", state_n]] + this.current_state = null; + this.states = {}; + var owner = this; + $.each(states, function(i,n) { + n[1].sm = owner; + owner.states[n[0]] = n[1]; + if (i==0) + owner.current_state = n[0]; + }); + if (!this.current_state) + throw "StateMachine must be initialized with at least one state"; + this.states[this.current_state].enter(); +}; +fb.state_machine.prototype = { + transition: function(to_state, exit_data, enter_data, data) { //fb.log("state_machine.transition current_state: ", this.current_state, "to_state: ", to_state); + // to_state: the target destination state + // exit_data: the exit data for current state + // enter_data: the enter data for to_state + // data: the data for to_state.handle + var target = this.states[to_state]; + if (!target) + throw("Unrecongized state:" + to_state); + + var source = this.states[this.current_state]; + + // exit current state + source.exit(exit_data); + + // enter target state + target.enter(enter_data); + + this.current_state = to_state; + + // handle data + this.handle(data); + }, + handle: function(data) { + if (data) + this.states[this.current_state].handle(data); + } +}; + + +/** + * InputControl class + */ +fb.InputControl = function() { + this.default_options = {}; + this.counter = 0; + this.cache = {}; + this.option_hash = {}; + this.sm = null; + this.delegates = {}; + this.dropdown_delay = 30; + this.manage_delay = 20; + this.release_delay = 10; +}; + +fb.InputControl.prototype = { + /** + * facility to reuse delegate functions with different arguments + */ + delegate: function(fname, argArray) { + if (!this.delegates[fname]) + this.delegates[fname] = fb.delegate(this[fname], this); + this.delegates[fname].argArray = argArray ? argArray : []; + return this.delegates[fname]; + }, + + options: function(input) {//fb.log("this.options", input); + var o = this.option_hash[input.fb_id]; + if (!o) + throw "Unknown input"; + return o; + }, + + transition: function(state) { + if (this.sm) + this.sm.transition(state); + }, + + handle: function(data) { + if (this.sm) + this.sm.handle(data); + }, + + /** + * get input value, if null, return empty string ("") + */ + val: function(input) { + var v = $(input).val(); + if (v == null) + return ""; + return $.trim(v); + }, + + /** + * get "name" or "text" field of an object. if none return "unknown" + */ + name: function(obj) { + // backwards compatibility with data.text and data.name + if (obj.text != null) + return obj.text; + if (obj.name != null) + return obj.name; + return "unknown"; + }, + + /** + * text change delay variable to length of string + */ + delay: function(l) { + var t = .3; + if (l > 0) + t = 1/(6 * (l-0.7)) + .3; + return t * 1000; + }, + + manage: function(input) { + this.release(input); + var owner = this; + $.each(["blur", "keydown", "keypress", "keyup", "input", "paste"], function(i,n) { + $(input).bind(n, owner.delegate(n)); + }); + this.transition("start"); + //this.handle({id:"TEXTCHANGE", input:input}); + this.manage_hook(input); + }, + + // over-ride to handle manage + manage_hook: function(input) {}, + + release: function(input) {//fb.log("release", input); + var owner = this; + $.each(["blur", "keydown", "keypress", "keyup", "input", "paste"], function(i,n) { + $(input).unbind(n, owner.delegate(n)); + }); + + this.transition("start"); + this.release_hook(input); + }, + + // over-ride to handle release + release_hook: function(input) {}, + + destroy: function(input) { + //Clear all timeouts + window.clearTimeout(this.manage_timeout); + window.clearTimeout(this.release_timeout); + window.clearTimeout(this.textchange_timeout); + window.clearTimeout(this.loadmsg_timeout); + + //Clear all event binding + var owner = this; + $.each(["focus", "click"], function(i,n) { + $(input).unbind(n, owner.delegate(n)); + }); + this.release(input); + }, + + focus: function(e) {//fb.log("on_focus", e); + window.clearTimeout(this.manage_timeout); + var input = e.target; + try { + this.options(input); + } + catch(e) { + return; + } + this.manage_timeout = window.setTimeout(this.delegate("manage", [input]), this.manage_delay); + }, + + blur: function(e) {//fb.log("on_blur", e.target, this, this.dont_release, this._input); + window.clearTimeout(this.release_timeout); + var input = $(e.target)[0]; + if (this.dont_release) { + // the current input we are losing focus on + // because we've clicked on the list/listitem + this._input = input; + return; + } + this.release_timeout = window.setTimeout(this.delegate("release", [input]), this.release_delay); + }, + + keydown: function(e) {//fb.log("on_keydown", e.keyCode); + switch(e.keyCode) { + case 9: // tab + this.tab(e); + break; + case 38: // up + case 40: // down + // prevents cursor/caret from moving (in Safari) + e.preventDefault(); + break; + default: + break; + } + }, + + keypress: function(e) {//fb.log("on_keypress", e.keyCode); + switch(e.keyCode) { + case 38: // up + case 40: // down + // prevents cursor/caret from moving + if (!e.shiftKey) + e.preventDefault(); + break; + case 13: // return + this.enterkey(e); + break ; + case 27: // escape + this.escapekey(e); + break; + default: + break; + } + }, + + keyup: function(e) {//fb.log("on_keyup", e.keyCode); + switch(e.keyCode) { + case 38: // up + e.preventDefault(); + this.uparrow(e); + break; + case 40: // down + e.preventDefault(); + this.downarrow(e); + break; + case 9: // tab + case 13: // enter + case 16: // ctrl + case 17: // shift + case 18: // option/alt + case 27: // escape + case 37: // left + case 39: // right + case 224:// apple/command + break; + default: + this.textchange(e); + break; + } + }, + + click: function(e) { + //Override this to handle mouseclicks + }, + + // Mozilla only, to detech paste + input: function(e) {//fb.log("on_input", e); + this.textchange(e); + }, + + // IE only, to detect paste + paste: function(e) {//fb.log("on_paste", e); + this.textchange(e); + }, + + uparrow: function(e) { + this.handle({id:"UPARROW", input:e.target}); + }, + + downarrow: function(e) { + this.handle({id:"DOWNARROW", input:e.target}); + }, + + tab: function(e) { + this.handle({id:"TAB", input:e.target, domEvent:e}); + }, + + enterkey: function(e) { + if(e.shiftKey) { + this.handle({id:"ENTERKEY-SHIFT", input:e.target, domEvent:e}); + } else { + this.handle({id:"ENTERKEY", input:e.target, domEvent:e}); + } + }, + + escapekey: function(e) { + this.handle({id:"ESCAPEKEY", input:e.target}); + }, + + textchange: function(e) {//fb.log("on_textchange", e.target); + window.clearTimeout(this.textchange_timeout); + // Save inputted text + this.input_text = this.val(e.target); + var delay = this.delay(this.input_text.length); + this.textchange_timeout = window.setTimeout(this.delegate("textchange_delay", [e.target]), delay); + }, + + textchange_delay: function(input){//fb.log("on_textchange_delay", input); + this.handle({id:"TEXTCHANGE", input:input}); + } +}; + +/** + * InputSelectControl class + * superClass: InputControl + */ +fb.InputSelectControl = function() { + fb.InputControl.call(this); + this.min_len = 1; + this.should_filter = true; + this.fudge = 8; + this.loadmsg_delay = 50; + + /** + * initialize the select state machine + * + * states: + * start: + * getting: + * selecting: + */ + this.sm = new fb.state_machine([ + ["start", new state_start(this)], + ["getting", new state_getting(this)], + ["selecting", new state_selecting(this)] + ]); +}; +// inheritance: prototype/constructor chaining +fb.InputSelectControl.prototype = new fb.InputControl(); +fb.InputSelectControl.prototype.constructor = fb.InputSelectControl; + +// shorthand for fb.InputSelectControl.prototype +var p = fb.InputSelectControl.prototype; + +p.get_list = function(){ + return $("#fbs_list > .fbs-bottomshadow > .fbs-ul")[0]; +}; + +p.get_list_items = function(){ + return $(this.get_list()).children("li"); +}; + +p.release_hook = function(input) { + this.list_hide(); +}; + +p.click_listitem = function(li) {//fb.log("click_listitem", li, this._input); + this.handle({id:"LISTITEM_CLICK", item:li, input:this._input}); +}; + +p.mousedown_list = function(e) {//fb.log("mousedown_list", e, this); + // hack in IE/safari to keep suggestion list from disappearing when click/scrolling + this.dont_release = true; +}; + +p.mouseup_list = function(e) {//fb.log("mouseup_list", e, this, this._input); + // hack in IE/safari to keep suggestion list from disappearing when click/scrolling + if (this._input) { + $(this._input).unbind("focus", this.delegate("focus")); + $(this._input).focus(); + window.setTimeout(this.delegate("reset_focus", [this._input]), 0); + //$(this._input).focus(this.delegate("focus")); + } + this.dont_release = false; +}; + +p.reset_focus = function(input) { + $(input).focus(this.delegate("focus")); +}; + +p.list_load = function(input) {//fb.log("list_load", input); + throw "You must override InputSelectControl.prototype.list_load"; +}; + +p.list_receive = function(input, txt, o) {//fb.log("list_receive", input, query, o); + // handle errors + if (o.status !== '200 OK') { + fb.error("list_receive", o.code, o.messages, o); + return; + } + + // currently, list_receive recognizes results of the forms: + // 1. { list: { listItems: [...] } } + // 2. { results: [...] } + // 3. { result: [...] } + // 4. { query: { result: [...] } } + var result = []; + if ("list" in o && "listItems" in o.list) + result = o.list.listItems; + else if ("result" in o) + result = o.result; + else if ("results" in o) + result = o.results; + else if ("query" in o && "result" in o.query) + result = o.query.result; + else { + fb.error("list_receive", o.code, "Unrecognized list result", o); + return; + } + + // hook to update cache + this.list_receive_hook(input, txt, result); + + // handle result + this.handle({id:"LIST_RESULT", input:input, result:result}); +}; + +p.list_receive_hook = function(input, txt, result) { + // overwrite to process search result + // like updating the cache +}; + +p.position = function(element, input) { + var options = this.options(input); + var pos = $(input).offset({border: true, padding: true}); + var left = pos.left; + var top = pos.top + input.clientHeight + this.fudge; + + var right = pos.left + options.width; + var window_right = $(window).width() + document.body.scrollLeft; + + // If the right edge of the dropdown extends beyond the right + // of the screen then right-justify the dropdown to the input. + if (right > window_right && options.width > $(input).outerWidth()) { + left = left - (options.width - $(input).outerWidth()) + 4; + } + $(element) + .css({top:top, left:left, width:options.width}) + .show(); +} + +p.list_show = function(input, result) {//fb.log("list_show", input, result); + if (!input) + return; + if (!result) + result = []; + var options = this.options(input); + var txt = this.val(input); + var list = null; + if (!$("#fbs_list").length) { + $(document.body) + .append( + '<div style="display:none;position:absolute" id="fbs_list" class="fbs-topshadow">' + + '<div class="fbs-bottomshadow">'+ + '<ul class="fbs-ul"></ul>' + + '</div>' + + '</div>'); + + list = $("> .fbs-ul")[0]; + } + if (!list) + list = this.get_list(); + + $("#fbs_list > .fbs-bottomshadow") + .unbind() + .mousedown(this.delegate("mousedown_list")) + .mouseup(this.delegate("mouseup_list")) + .scroll(this.delegate("mousedown_list")); + + + // unbind all li events and empty list + $("li", list) + .each(function() { + $(this).unbind(); + this.fb_data = null; // clean up expando variable "fb_data" + }); + $(list).empty(); + + var filter = this.filter; + if (typeof options.filter == "function") + filter = options.filter; + + if (!result.length) + $(list).append(this.create_list_item({id:"NO_MATCHES", text:"no matches"}, null, options).addClass("fbs-li-nomatch")); + + var filtered = []; + if(this.should_filter) { + $.each(result, function(i, n) { + if (filter.apply(null, [n, txt])) + filtered.push(n); + }); + filtered = this.filter_hook(filtered, result); + } else { + filtered = result; + } + var owner = this; + $.each(filtered, function(i, n) { + $(list).append(owner.create_list_item(n, txt, options, i)); + }); + + // hook to add additional html elemments and handlers + // like "Create New" item under the list + this.list_show_hook(list, input, options); + + this.position($("#fbs_list"), input); +}; + +p.list_show_hook = function(list, input, options) { }; + +p.filter_hook = function(filtered, result) { + return filtered; +}; + +p.list_hide = function() {//fb.log("list_hide"); + $("#fbs_list").hide(); + this.list_hide_hook(); +}; + +p.list_hide_hook = function() {}; + +p.create_list_item = function(data, txt, options, index) { + var li = $("<li class='fbs-li'></li>")[0]; + + var trans = this.transform; + if (typeof options.transform == "function") + trans = options.transform; + + var html = trans.apply(this, [data, txt]); + + $(li).append(html); + + data.index = index; + + // sometimes data contains text and/or name + if ("text" in data) + data.name = data.text; + + li.fb_data = data; + + var owner = this; + return $(li) + .mouseover(function(e) { + owner.list_select(null, this, options); + }) + .click(function(e) { + owner.click_listitem(this); + }); +}; + +/** + * The default filter + * + * @param data - The individual item from the ac_path service. + * @return TRUE to include in list or FALSE to exclude from list. + */ +p.filter = function(data, txt) { + return true; +}; + +/** + * The default transform + * + * @param data - The individual item from the ac_path service + * @param txt - The input string + * @param options - Options used for the input + * @return a DOM element or html that will be appended to an <li/> + */ +p.transform = function(data, txt) { + return data; +}; + +/** + * show loading message + */ +p.loading_show = function(input) { + this.list_hide(); + if (!$("#fbs_loading").length) { + $(document.body) + .append( + '<div style="display:none;position:absolute" id="fbs_loading" class="fbs-topshadow">' + + '<div class="fbs-bottomshadow">'+ + '<ul class="fbs-ul">' + + '<li class="fbs-li">'+ + '<div class="fbs-li-name">loading...</div>' + + '</li>' + + '</ul>' + + '</div>' + + '</div>'); + } + this.position($("#fbs_loading"), input); +}; + +/** + * hide loading message + */ +p.loading_hide = function() { + $("#fbs_loading").hide(); +}; + +p.list_select = function(index, li, options) { + var sli = null; + this.get_list_items().each(function(i,n) { + if (i == index || li == n) { + $(n).addClass("fbs-li-selected"); + sli = n; + } + else + $(n).removeClass("fbs-li-selected"); + }); + this.list_select_hook(sli, options); + return sli; +}; + +/** + * list select hook + * @param sli - list item (li) html element + */ +p.list_select_hook = function(sli, options) { }; + +p.list_length = function() { + return this.get_list_items().length; +}; + +p.list_selection = function(returnObj) { + if (!returnObj) + returnObj = {}; + returnObj.index = -1; + returnObj.item = null; + this.get_list_items().each(function(i,n){ + if (n.className.indexOf("fbs-li-selected") != -1) { + returnObj.index = i; + returnObj.item = n; + return false; + } + }); + return returnObj; +} + +p.list_select_next = function(options, data) { + var len = this.list_length(); + var obj = this.list_selection(); + var index = obj.index+1; + if (index >=0 && index < len){ + var sel = this.list_select(index, null, options); + // Change input value to reflect selected item + var txt = $(".fbs-li-name", sel).text(); + $(data.input).val(txt); + return sel + } else if (options.soft) { + // Since no item is currently selected, + // change input value to reflect previously inputted text + $(data.input).val(this.input_text); + return this.list_select(null, null, options); + } else if (len > 0) { + return this.list_select(0, null, options); + } + return null; +}; + +p.list_select_prev = function(options, data) { + var len = this.list_length(); + var obj = this.list_selection(); + var index = obj.index-1; + if (index >=0 && index < len) { + var sel = this.list_select(index, null, options); + // Change input value to reflect selected item + var txt = $(".fbs-li-name", sel).text(); + $(data.input).val(txt); + return sel + } else if (options.soft) { + if (index < -1 && len > 0) { + return this.list_select(len - 1, null, options); + } else { + // Since no item is currently selected, + // change input value to reflect previously inputted text + $(data.input).val(this.input_text); + return this.list_select(null, null, options); + } + } else if (len > 0) + return this.list_select(len - 1, null, options); + return null; +}; + +p.scroll_into_view = function(elt, p) { + if (!elt) + return; + if (!p) + p = elt.parentNode; + if (!p) { + elt.scrollIntoView(false); + return; + } + + if (elt.offsetTop < p.scrollTop) { + p.scrollTop = elt.offsetTop; + return; + } + + var elt_h = $(elt).height(); + var p_h = $(p).height(); + if ((elt.offsetTop + elt_h) > (p.scrollTop + p_h)) { + p.scrollTop = elt.offsetTop + elt_h - p_h; + } +}; + +/** + * emphasize part of the html text with <em/> + */ +p.em_text = function(text, em_str) { + var em = text; + var index = text.toLowerCase().indexOf(em_str.toLowerCase()); + if (index >= 0) { + em = text.substring(0, index) + + '<em class="fbs-em">' + + text.substring(index, index+em_str.length) + + '</em>' + + text.substring(index + em_str.length); + } + return em; +}; + +p.caret_last = function(input) { + var l = this.val(input).length; + if (input.createTextRange) { + // IE + var range = input.createTextRange();; + range.collapse(true); + range.moveEnd("character", l); + range.moveStart("character", l); + range.select(); + } + else if (input.setSelectionRange) { + // mozilla + input.setSelectionRange(l, l); + } +}; + +/** + * base class for all select states + * @param c:InputSelectControl + */ +function select_state(c) { + fb.state.call(this); + this.c = c; +}; +// inheritance: prototype/constructor chaining +select_state.prototype = new fb.state(); +select_state.prototype.constructor = select_state; + +/** + * state: start + */ +function state_start(c) { + select_state.call(this, c); +}; +// inheritance: prototype/constructor chaining +state_start.prototype = new select_state(); +state_start.prototype.constructor = state_start; + +state_start.prototype.handle = function(data) {//fb.log("state_start.handle", data); + if (!data || !data.input) + return; + var options = this.c.options(data.input); + switch (data.id) { + case "TEXTCHANGE": + case "DOWNARROW": + var txt = this.c.val(data.input); + if (txt.length >= this.c.min_len) + this.sm.transition("getting", null, data); + else + this.c.list_hide(); + break; + case "DROPDOWN": + this.sm.transition("getting", null, data); + break; + case "ENTERKEY": + $(data.input).trigger("fb-noselect", [data]); + this.sm.transition("start"); + window.clearTimeout(this.c.textchange_timeout); + break; + case "ENTERKEY-SHIFT": + data.domEvent.preventDefault(); + break; + default: + break; + }; +}; + +/** + * state: getting + */ +function state_getting(c) { + select_state.call(this, c); +}; +// inheritance: prototype/constructor chaining +state_getting.prototype = new select_state(); +state_getting.prototype.constructor = state_getting; + +state_getting.prototype.enter = function(data) {//fb.log("state_getting.enter", data); + window.clearTimeout(this.c.loadmsg_timeout); + if (!data || !data.input) + return; + // show loading msg + this.c.loadmsg_timeout = window.setTimeout(this.c.delegate("loading_show", [data.input]), this.c.loadmsg_delay); + // request autocomplete url + this.c.list_load(data.input); +}; +state_getting.prototype.exit = function(data) {//fb.log("state_getting.exit", data); + // hide loading msg + window.clearTimeout(this.c.loadmsg_timeout); + this.c.loading_hide(); +}; +state_getting.prototype.handle = function(data) {//fb.log("state_getting.handle", data); + if (!data || !data.input) + return; + var options = this.c.options(data.input); + switch (data.id) { + case "TEXTCHANGE": + this.sm.transition("start", null, null, data); + break; + case "LIST_RESULT": + this.sm.transition("selecting", null, data); + break; + case "ENTERKEY": + $(data.input).trigger("fb-noselect", [data]); + this.c.list_hide(); + this.sm.transition("start"); + window.clearTimeout(this.c.textchange_timeout); + break; + case "ENTERKEY-SHIFT": + data.domEvent.preventDefault(); + break; + case "ESCAPEKEY": + this.c.list_hide(); + this.sm.transition("start"); + break; + default: + break; + }; +}; + +/** + * state: selecting + */ +function state_selecting(c) { + select_state.call(this, c); +}; +// inheritance: prototype/constructor chaining +state_selecting.prototype = new select_state(); +state_selecting.prototype.constructor = state_selecting; + +state_selecting.prototype.enter = function(data) {//fb.log("state_selecting.enter", data); + if (!data || !data.input || !data.result) + return; + this.c.list_show(data.input, data.result); + var options = this.c.options(data.input); + if (!options.soft) + this.c.list_select(0, null, options); +}; +state_selecting.prototype.exit = function(data) {//fb.log("state_selecting.exit", data); + this.c.list_select(null); +}; +state_selecting.prototype.handle = function(data) {//fb.log("state_selecting.handle", data); + if (!data || !data.input) + return; + var options = this.c.options(data.input); + switch (data.id) { + case "TEXTCHANGE": + this.c.should_filter = true; + this.sm.transition("start", null, null, data); + break; + case "DOWNARROW": + $("#fbs_list").show(); + var li = this.c.list_select_next(options, data); + this.c.scroll_into_view(li); + break; + case "UPARROW": + $("#fbs_list").show(); + var li = this.c.list_select_prev(options, data); + this.c.scroll_into_view(li); + break; + case "DROPDOWN": + if(this.c.val(data.input) == "") { + this.sm.transition("start"); + this.c.list_hide(); + } else { + this.sm.transition("getting", null, data); + this.c.should_filter = !this.c.should_filter; + } + break; + case "TAB": + var s = this.c.list_selection(); + if (s.index == -1 || !s.item) { + $(data.input).trigger("fb-noselect", [data]); + return; + } + this.listitem_select(data.input, s.item); + break; + case "ENTERKEY-SHIFT": + // Create new topic if user is holding shift while hitting enter + this.c.create_new(data.input); + data.domEvent.preventDefault(); + break; + case "ENTERKEY": + var s = this.c.list_selection(); + if (s.index == -1 || !s.item) { + $(data.input).trigger("fb-noselect", [data]); + return; + } + if ($("#fbs_list").css("display") != "none"){ + data.domEvent.preventDefault(); + } else { + $(data.input).trigger("fb-submit", [s.item.fb_data]); + return; + } + this.listitem_select(data.input, s.item); + break; + case "LISTITEM_CLICK": + this.listitem_select(data.input, data.item); + break; + case "ESCAPEKEY": + this.c.list_hide(); + this.sm.transition("start"); + break; + default: + break; + }; +}; + +state_selecting.prototype.listitem_select = function(input, item) { + if (!item) + return; + switch(item.fb_data.id) { + case "NO_MATCHES": + break; + default: + var txt = $(".fbs-li-name", item).text(); + $(input).val(txt); + this.c.caret_last(input); + $(input).trigger("fb-select", [item.fb_data]) + .trigger("suggest", [item.fb_data]); // legacy - for compatibility + this.c.list_hide(); + break; + } +}; + + +function use_jsonp(options) { + // if we're on the same host, then we don't need to use jsonp. This + // greatly increases our cachability + if (!options.service_url) + return false; // no host == same host == no jsonp + var pathname_len = window.location.pathname.length; + var hostname = window.location.href; + var hostname = hostname.substr(0, hostname.length - pathname_len); + //console.log("Hostname = ", hostname); + if (hostname == options.service_url) + return false; + + return true; +} +/** + * freebaseSuggest() provides a way to attach Freebase suggestion behavior to a + * text input using the Freebase.com autocomplete service. + * + * freebaseSuggest accepts a single argument which is an options Object with + * the following attributes: + * + * width: This is the width of the suggestion list and the flyout in + * pixels. Default is 275. + * + * soft: Soft suggestion. If true, DO NOT auto-select first item + * in the suggestion list. Otherwise, select first item. + * Default is false. + * + * suggest_new: To enable a suggest new option, set text to a non-null string. + * This is the string displayed for the suggest new option + * (eg, "Create new topic"). Default is null. + * + * flyout: To enable flyout to show additional information for the + * currently highlighted item including a thumbnail and blurb. + * Default is true. + * + * service_url: This the base url to all the api services like autocomplete, + * blurbs and thumbnails. Default is "http://www.freebase.com". + * + * ac_path: The path to the autcomplete service. Default is "/api/service/search". + * + * ac_param: A dicionary of query parameters to the autocomplete service. + * Currently, the supported parameters are + * query (required) - the string to do an auto-complete on. See ac_qstr + * type (optional) - type of items to match for (ie, "/film/film") + * limit (optional) - the maximum number of results to return, default is 20 + * start (optional) - offset from which to start returning results, default is 0 + * + * ac_qstr: This is the parameter name to be passed to the autocomplete + * service for the string to autocomplete on. The value will + * be what the user typed in the input. Default is "prefix". + * + * blurb_path: The path to the blurb service for the description to be shown + * in the flyout. Default is "/api/trans/blurb". + * + * blurb_param: The query parameters to the blurb service. + * Default is { maxlength: 300 }. + * + * thumbnail_path: The path to the thumbnail service to be shown in the flyout. + * Default is "/api/trans/image_thumb". + * + * thumbnail_param: The query paramters to the thumbnail service. + * Default is {maxwidth:70, maxheight: 70}. + * + * filter: Specify a filter function if you want to filter any of the items + * returned by ac_path service. The function is called with one + * arugment representing an item from the ac_path result. The function + * should return TRUE to include the item or FALSE to exclude. + * Default is a function that returns TRUE. + * + * transform: Specify a transform function if you want to transform the default + * display of the suggest list item. + * + * initialize: Set to true for control to be managed from the start. Use when you know + * that the control started with the focus. (Default: false) + * + * In addition, freebaseSuggest will trigger the following events on behalf of + * the input it's attached to. They include: + * + * fb-select: Triggered when something is selected from the suggestion + * list. The data object will contain id and name fields: + * { id: aString, name: aString }. + * + * fb-select-new: Triggered when the suggest_new option is selected. + * The data object will only contain a name field: { name: aString }. + * + * fb-noselect: Triggered when the user performs a select action without highlighting an entry. + * + * + * @example + * $('#myInput') + * .freebaseSuggest() + * .bind('fb-select', function(e, data) { console.log('suggest: ', data.id); }) + * + * @desc Attach Freebase suggestion behavior to #myInput with default options and on + * 'suggest', output the selected id the console. + * + * + * @example + * var options = { + * soft: true, + * suggest_new: 'Create new Film', + * ac_param: { + * type: '/film/film', + * category: 'instance', + * disamb: '1', + * limit: '10' + * } + * }; + * $('#myInput') + * .freebaseSuggest(options) + * .bind('fb-select', function(e, data) { console.log('suggest: ', data.id); }) + * .bind('fb-select-new', function(e, data) { console.log('suggest new: ', data.name); }); + * + * @desc Soft suggestion on instances of '/film/film' with a suggest new option and + * output the various events to the console. + * + * @name freebaseSuggest + * @param options object literal containing options which control the suggestion behavior + * @return jQuery + * @cat Plugins/Freebase + * @type jQuery + */ +$.fn.freebaseSuggest = function(action, options) { + if ((typeof action == "undefined" || typeof action == 'object') && options == null) { + // Only passed options so assume activation + options = action; + action = "activate"; + } + + if (action == 'activate') { + $(this)._freebaseInput(fb.suggest.getInstance(), options); + } else if (action == 'destroy') { + fb.suggest.getInstance().destroy(this); + } + + return $(this); +}; + +/** + * SuggestControl class + * superClass: InputSelectControl + */ +function SuggestControl() { + fb.InputSelectControl.call(this); + this.default_options = { + width: 275, // width of list and flyout + soft: true, // if true, DO NOT auto-select first item, otherwise select first item by default + suggest_new: null, // to show suggest new option, set text to something (eg, "Create new topic") + flyout: true, // show flyout on the side of highlighted item + service_url: "http://www.freebase.com", + ac_path: "/api/service/search", + ac_param: { + type: "/common/topic", + start: 0, + limit: 20 + }, + ac_qstr: "prefix", // this will be added to the ac_param ...&prefix=str + blurb_path: "/api/trans/blurb", + blurb_param: { + maxlength: 300 + }, + thumbnail_path: "/api/trans/image_thumb", + thumbnail_param: {maxwidth: 70, maxheight: 70}, + filter: null, + transform: null, + initialize: false + }; +}; +// inheritance: prototype/constructor chaining +SuggestControl.prototype = new fb.InputSelectControl(); +SuggestControl.prototype.constructor = SuggestControl; + +SuggestControl.instance = null; +SuggestControl.getInstance = function() { + if (!SuggestControl.instance) + SuggestControl.instance = new SuggestControl(); + return SuggestControl.instance; +}; + +// shorthand for SuggestControl.prototype +var p = SuggestControl.prototype; + +p.list_load = function(input) {//fb.log("list_load", input); + if (!input) + return; + if (!"fb_id" in input) + return; + var txt = this.val(input); + if (!txt.length) + return; + if (!this.cache[input.fb_id]) + this.cache[input.fb_id] = {}; + if (txt in this.cache[input.fb_id]) { + //fb.log("load from cache: ", txt); + window.clearTimeout(this.handle_timeout); + this.handle_timeout = window.setTimeout(this.delegate("handle", [{id:"LIST_RESULT", input:input, result:this.cache[input.fb_id][txt]}]), 0); + return; + } + var options = this.options(input); + var txt = this.val(input); + var param = options.ac_param; + //TODO: remove star and change ac_qstr when search gets the same params as autocomplete + param[options.ac_qstr] = txt; // + '*'; // the search api needs a '*' to perform auto-complete rather than search. + // dae: no longer needed if you use the "prefix" parameter + $.ajax({ + type: "GET", + url: options.service_url + options.ac_path, + data: param, + success: this.delegate("list_receive", [input, txt]), + dataType: use_jsonp(options) ? "jsonp": "json", + cache: true + }); +}; + +p.list_receive_hook = function(input, txt, result) { + // update cache + if (!this.cache[input.fb_id]) + this.cache[input.fb_id] = {}; + this.cache[input.fb_id][txt] = result; +}; + +/** + * add select new option below the select list + * and attach mouseover, mouseout, and click handlers + */ +p.list_show_hook = function(list, input, options) { + if (!$(list).next(".fbs-selectnew").length) + $(list).after('<div style="display: none;" class="fbs-selectnew"></div>'); + var suggest_new = $(list).next(".fbs-selectnew"); + if (options.suggest_new) { + var owner = this; + // Create description, button and shortcut text + $(suggest_new) + .unbind() + .empty() + .append('<div class="fbs-selectnew-description">Your item not in the list?</div><button type="submit" class="fbs-selectnew-button"></button><span class="fbs-selectnew-shortcut">(Shift+Enter)</span>') + .mouseover(function(e) { + owner.list_select(null); + owner.flyout_hide(); + }); + + // Create and title create new button + var suggest_new_button = suggest_new.find(".fbs-selectnew-button").eq(0); + $(suggest_new_button) + .unbind() + .empty() + .append(options.suggest_new) + .click(function(e) { + owner.create_new(input); + }); + + // Display create new box + suggest_new.show(); + } + else + $(suggest_new).unbind().hide(); +}; + +p.list_hide_hook = function() { + this.flyout_hide(); +}; + +p.list_select_hook = function(sli, options) { + this.flyout_hide(); + if (sli && options && options.flyout && sli.fb_data && sli.fb_data.id != "NO_MATCHES") + this.flyout(sli, options); +}; + +p.create_new = function(input){ + $(input).trigger("fb-select-new", [{name:this.val(input)}]) + .trigger("suggest-new", [{name:this.val(input)}]); // legacy - for compatibility + this.list_hide(); + this.transition("start"); +} + +p.transform = function(data, txt) { + var owner = this; + var types = []; + if (data.type) + $.each(data.type, function(i,n){ + if (n.id != '/common/topic') + types.push(owner.name(n)); + }); + types = types.join(", "); + + var domains = []; + if (data.domain) + $.each(data.domain, function(i,n){ + domains.push(owner.name(n)); + }); + domains = domains.join(", "); + + var aliases = []; + if (data.alias) + $.each(data.alias, function(i,n){ + aliases.push(n); + }); + aliases = aliases.join(", "); + + var props = []; + if (data.properties) + $.each(data.properties, function(i,n){ + props.push(n); + }); + props = props.join(", "); + + var div = document.createElement("div"); + $(div).append( + '<div class="fbs-li-aliases"></div>' + + '<div class="fbs-li-name"></div>' + + '<div class="fbs-li-types"></div>' + + '<div class="fbs-li-domains"></div>' + + '<div class="fbs-li-props"></div>'); + if (aliases.length) { + var text = $(".fbs-li-aliases", div).append(document.createTextNode("("+aliases+")")).text(); + if (txt) + $(".fbs-li-aliases", div).empty().append(this.em_text(text, txt)); + } + else + $(".fbs-li-aliases", div).remove(); + + var text = $(".fbs-li-name", div).append(document.createTextNode(this.name(data))).text(); + if (txt) + $(".fbs-li-name", div).empty().append(this.em_text(text, txt)); + + if (types.length) + $(".fbs-li-types", div).append(document.createTextNode(types)); + else + $(".fbs-li-types", div).remove(); + + if (domains.length) + $(".fbs-li-domains", div).append(document.createTextNode(domains)); + else + $(".fbs-li-domains", div).remove(); + + if (props.length) + $(".fbs-li-props", div).append(document.createTextNode(props)); + else + $(".fbs-li-props", div).remove(); + + return div.innerHTML; +}; + +p.flyout = function(li, options) { //fb.log("flyout", li); + this.flyout_callback = this.flyout_resources(li, options); +}; + +/** + * load flyout resources (thumbnail, blurb), don't show until + * both thumbnail and blurb have been loaded. + */ +p.flyout_resources = function(li, options) {//fb.log("flyout_resources", li); + window.clearTimeout(this.flyout_resources_timeout); + //this.handle_timeout = window.setTimeout(this.delegate("handle", [{id:"LIST_RESULT", input:input, result:this.cache[input.fb_id][txt]}]), 0); + this.flyout_resources_timeout = window.setTimeout(this.delegate("flyout_resources_delay", [li, options]), 100); +} + +p.flyout_resources_delay = function(li, options) { + var data = li.fb_data; + var data_types = ["article", "image"]; + var cb = new FlyoutResourcesHandler(this, li, options); + return cb; +}; + +p.flyout_hide = function() {//fb.log("flyout_hide"); + if (this.flyout_callback) + this.flyout_callback.destroy(); + $("#fbs_flyout").hide(); +}; + +p.flyout_show = function(li, options, img_src, blurb) {//fb.log("flyout_show", li, img_src, blurb); + if ("none" == $("#fbs_list").css("display")) + return; + var s = this.list_selection().item; + if (!(li == s && li.fb_data.id == s.fb_data.id)) + return; + + if (!$("#fbs_flyout").length) { + $(document.body) + .append( + '<div style="display:none;position:absolute" id="fbs_flyout" class="fbs-topshadow">' + + '<div class="fbs-bottomshadow">'+ + '<div class="fbs-flyout-container">' + + // label + '<div class="fbs-flyout-name"></div>' + + // image + '<div class="fbs-flyout-image"></div>' + + // types + '<div class="fbs-flyout-types"></div>' + + // domains + '<div class="fbs-flyout-domains"></div>' + + // blurb + '<div class="fbs-flyout-blurb"></div>' + + '</div>' + + '</div>' + + '</div>'); + } + + $("#fbs_flyout .fbs-flyout-name").empty().append('<a href="' + this.freebase_url(li.fb_data.id, options) + '">' + $(".fbs-li-name", li).text() + '</a>'); + $("#fbs_flyout .fbs-flyout-image").empty(); + if (img_src != "#") + $("#fbs_flyout .fbs-flyout-image").append('<img src="' + img_src + '"/>'); + $("#fbs_flyout .fbs-flyout-types").empty().append($(".fbs-li-types", li).text()); + $("#fbs_flyout .fbs-flyout-domains").empty().append($(".fbs-li-domains", li).text()); + $("#fbs_flyout .fbs-flyout-blurb").empty().append(blurb); + + var pos = $(this.get_list()).offset(); + var left = pos.left + options.width; + var sl = document.body.scrollLeft; + var ww = $(window).width(); + if ((left+options.width) > (sl+ww)) + left = pos.left - options.width; + //var pos = $(li).offset(); + $("#fbs_flyout") + .css({top:pos.top, left:left, width:options.width}) + .show(); +}; + +p.freebase_url = function(id, options) { + var url = options.service_url + "/view" + fb.quote_id(id); + return url; +}; + +p.release_hook = function(input){ + $(this.get_list()).next(".fbs-selectnew").remove(); + fb.InputSelectControl.prototype.release_hook.call(this, input); +}; + +/** + * We don't want to show the flyout until both the article (blurb) + * and the image have been loaded. This object is an attempt to + * encapsulate the loading of these two flyout resources, waits for both + * resources to load and then finally calls SuggestControl.flyout_show. + * + * @param owner - SuggestControl + * @param li - the selected list item that we are showing the flyout for + * @param options - SuggestControl settings + */ +function FlyoutResourcesHandler(owner, li, options) { + this.owner = owner; + this.li = li; + this.options = options; + var me = this; + $.each(["article", "image"], function(i,n) { + var id = li.fb_data[n]; + if (id && typeof id == 'object') + id = id.id; + me["load_" + n](id); + }); + +}; +FlyoutResourcesHandler.prototype = { + load_article: function(id) { + if (id) { + $.ajax({ + type: "GET", + url: this.blurb_url(id), + data: this.options.blurb_param, + success: fb.delegate(this.receive_article, this), + error: fb.delegate(this.receive_article_error, this), + dataType: use_jsonp(this.options) ? "jsonp" : null, + cache: true + }); + } + else { + this.receive("article", " "); + } + }, + receive_article: function(o) { + if (typeof o == "object") { + // handle errors + if (o.status !== '200 OK') { + fb.error("SuggestControl.blurb_receive", o.code, o.messages, o); + return; + } + + // now get the string value + o = o.result.body; + } + this.receive("article", o); + }, + receive_article_error: function() { + this.receive("article", "Description could not be displayed"); + }, + load_image: function(id) { + if (id) { + var i = new Image(); + var src = this.thumbnail_url(id); + i.onload = fb.delegate(this.receive_image, this, [src, i]); + i.onerror = fb.delegate(this.receive_image, this, ["#", i]); + i.src = src; + } + else { + this.receive("image", "#"); + } + }, + receive_image: function(src, i) { + this.receive("image", src); + // clean up Image.onload and onerror handlers + i.onload = i.onerror = null; + }, + blurb_url: function(id) { + return this.options.service_url + this.options.blurb_path + fb.quote_id(id); + }, + thumbnail_url: function(id) { + var url = this.options.service_url + this.options.thumbnail_path + + fb.quote_id(id); + var qs = $.param(this.options.thumbnail_param); + if (qs) + url += "?" + qs; + return url; + }, + receive: function(data_type, data) { + if (!this.owner) return; + this[data_type] = {data: data}; + if (this.image && this.article) + this.owner.flyout_show(this.li, this.options, this.image.data, this.article.data); + }, + destroy: function() { + this.owner = this.li = this.options = this.image = this.article = null; + } +}; + +fb.suggest = SuggestControl; + +})(jQuery, window.freebase.controls); diff --git a/examples/freebase-images-appengine/stylesheets/button_bkd.gif b/examples/freebase-images-appengine/stylesheets/button_bkd.gif Binary files differnew file mode 100644 index 0000000..db2671b --- /dev/null +++ b/examples/freebase-images-appengine/stylesheets/button_bkd.gif diff --git a/examples/freebase-images-appengine/stylesheets/down-arrow.gif b/examples/freebase-images-appengine/stylesheets/down-arrow.gif Binary files differnew file mode 100644 index 0000000..3ada88f --- /dev/null +++ b/examples/freebase-images-appengine/stylesheets/down-arrow.gif diff --git a/examples/freebase-images-appengine/stylesheets/freebase-controls.css b/examples/freebase-images-appengine/stylesheets/freebase-controls.css new file mode 100644 index 0000000..97d34b6 --- /dev/null +++ b/examples/freebase-images-appengine/stylesheets/freebase-controls.css @@ -0,0 +1,205 @@ + +.fbs-enumcontrol { + background: #ffffff url(down-arrow.gif) no-repeat center right; + padding-right:12px; +} + +.fbs-topshadow { +/* font-family: Arial, Helvetica, sans-serif; */ + margin: -8px 0px 0px -4px; + padding:0; + display:block; + z-index:1000; + position:absolute; + background: url('shadow-top-left.png') no-repeat top left; +} + +.fbs-bottomshadow { + position: relative; + right: -4px; + bottom: -4px; + padding: 0px 4px 4px 0px; + background: url('shadow-bottom-right.png') no-repeat bottom right; +} +* html .fbs-bottomshadow, +* html .fbs-topshadow { + background:none; +} + +.fbs-flyout-container, +.fbs-ul { + margin: 0px; + padding: 2px 0px 0px 0px; + text-align: left; + list-style:none; + border: 1px solid #808080; + background-color: #ffffff; + /* font-size: .9em;*/ + filter:alpha(opacity=95); + -moz-opacity:.95; + opacity:.95; + overflow: auto; + overflow-x: hidden +} + +.fbs-ul { + max-height: 300px; +} + +* html .fbs-ul { + height: expression( this.scrollHeight > 300 ? "300" : "auto" ); /* sets max-height for IE */ +} + + +.fbs-li { + padding: 3px 2px 3px 4px; + margin: 0px; + font-size: .9em; +} + +.fbs-li-aliases, +.fbs-li-types, +.fbs-li-domains, +.fbs-li-name, +.fbs-li-props { + _width : 253px !important; /* TODO Move to conditional comments */ + overflow: hidden; + white-space:nowrap; + text-overflow:ellipsis; + display:block; +} + +.fbs-li-types, +.fbs-li-domains { + color: #5d5d5d; + font-size: .9em; + font-style: italic; +} + +.fbs-li-aliases { + float: right; + max-width: 40%; + vertical-align:bottom; +} + +* html .fbs-li-name{ + width: 100%; +} + +* html .fbs-li-aliases { + display:none; +} + +.fbs-li-aliases:before { + content:'...'; +} + +.fbs-li-props { + font-size: .9em; + color: #555; +} + +.fbs-li-nomatch { + font-style: italic; +} + +.fbs-selectnew { + color: #383838; + border: 1px solid #c5c5c5; + border-top: 1px solid #fff; + background-color: #f3f3f3; + font-size: .9em; + margin: 0px; + padding: .4em .3em .5em; + filter:alpha(opacity=90); + -moz-opacity:.90; + opacity:.90; +} + +.fbs-selectnew-button { + cursor: pointer; + background: url(button_bkd.gif) bottom left repeat-x white; + border: 1px solid #9a9a9a; + padding: .2em .3em; + margin-left: 0px !important; + max-width: 17em; +} + + +.fbs-selectnew-button:hover { + text-decoration: none; +} + +.fbs-selectnew-description { + margin-bottom: .6em; +} + +.fbs-selectnew-shortcut { + font-size: .9em; + color: #535353; + margin-left: .4em; +} + +.fbs-li-selected { + background-color:#97bedf; + color: #000000; +} + +.fbs-em { + font-style: normal; + font-weight: bold; +} + +.fbs-flyout-container { + padding: 2px .45em 2px 14px; + min-height: 1em; +} + +* html .fbs-flyout-container { + height: 1%; +} + +.fbs-flyout-name, +.fbs-flyout-blurb { + font-size: .9em; + padding: .3em 0px .1em; + line-height: 130%; + margin-bottom: .5em; +} + +.fbs-flyout-types, +.fbs-flyout-domains { + font-size: .85em; + padding: 0; + margin:.3em 0 .3em 0 !important; + font-weight:normal !important; + font-style: italic; + color: #9a9a9a; +} + +.fbs-flyout-image { + float:left; + width:auto; + margin-right: .3em; + padding: 3px; +} + +.fbs-flyout-image img { + vertical-align: middle; + padding: 3px; + border: 1px solid #c8c8c3; + background-color: #fff; +} + + +.fbs-flyout-name a { + color: #006EE9; + text-decoration:none; +} +.fbs-flyout-name a:hover { + color: #006EE9; + text-decoration:underline; +} +.fbs-flyout-name a:visited { + color:#800080; +} diff --git a/examples/freebase-images-appengine/stylesheets/main.css b/examples/freebase-images-appengine/stylesheets/main.css new file mode 100644 index 0000000..d568831 --- /dev/null +++ b/examples/freebase-images-appengine/stylesheets/main.css @@ -0,0 +1,24 @@ +body { + font-family: Helvetica, Georgia, sans-serif; + background-color: #eee; + width: 65%; margin: 0 auto; +} + + + +#input { text-align: center; border: 1px solid black; padding: 1em; margin: 1em 2em;} +#images { border: 1px solid black; margin: 1em 2em; padding: 1em;} +#footer { margin: 0 2em 1em; padding: .4em; background: #ddd; text-align: right;} +#freebase { background: #ddd; margin: .5em 2em; padding: .5em; } + +img { margin: .5em; } +a img { border: none;} + +h1 { font-size: 2em;} +#q { font-size: 1em;} + +.fbs-ul { font-size: 1.3em;} + +a { color: blue; } +a:visited { color:blue; } +a:link { color: blue; }
\ No newline at end of file diff --git a/examples/freebase-images-appengine/stylesheets/shadow-bottom-right.png b/examples/freebase-images-appengine/stylesheets/shadow-bottom-right.png Binary files differnew file mode 100644 index 0000000..5679a53 --- /dev/null +++ b/examples/freebase-images-appengine/stylesheets/shadow-bottom-right.png diff --git a/examples/freebase-images-appengine/stylesheets/shadow-top-left.png b/examples/freebase-images-appengine/stylesheets/shadow-top-left.png Binary files differnew file mode 100644 index 0000000..43c788e --- /dev/null +++ b/examples/freebase-images-appengine/stylesheets/shadow-top-left.png |
