From c6b1c16dc90834a630ef74b6330b8c2cd631040f Mon Sep 17 00:00:00 2001 From: alvaro <alvaro@alia.(none)> Date: Sat, 23 Jun 2012 22:34:38 -0700 Subject: [PATCH] Adding search capabilities based on rdfs:label --- .../services/classes/html.template | 41 +- .../services/instances/html.template | 39 +- .../services/namedGraphs/html.template | 43 +- .../services/search/html.template | 1 + .../services/search/json.template | 5 + .../services/search/queries/main.query | 11 + .../types/rdfs:Resource/html.template | 37 +- static/img/wait.gif | Bin 0 -> 847 bytes static/index.html | 29 +- static/js/bootstrap-typeahead.js | 311 ++++++++++++++ static/js/typeahead.js | 387 ++++++++++++++++++ 11 files changed, 877 insertions(+), 27 deletions(-) create mode 120000 doc/examples/originalComponents/services/search/html.template create mode 100644 doc/examples/originalComponents/services/search/json.template create mode 100644 doc/examples/originalComponents/services/search/queries/main.query create mode 100644 static/img/wait.gif create mode 100644 static/js/bootstrap-typeahead.js create mode 100644 static/js/typeahead.js diff --git a/doc/examples/originalComponents/services/classes/html.template b/doc/examples/originalComponents/services/classes/html.template index 6bc4a791..0737ba48 100644 --- a/doc/examples/originalComponents/services/classes/html.template +++ b/doc/examples/originalComponents/services/classes/html.template @@ -6,15 +6,39 @@ <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="description" content=""> <meta name="author" content=""> - <link href="css/bootstrap.min.css" rel="stylesheet" type="text/css" media="screen" /> + <link href="{{lodspk.home}}css/bootstrap.min.css" rel="stylesheet" type="text/css" media="screen" /> <style> body { padding-top: 60px; /* 60px to make the container go all the way to the bottom of the topbar */ } + .wait{ + background-image:url('{{lodspk.home}}img/wait.gif'); + background-repeat:no-repeat; + padding-right:20px; + background-position: right; + } </style> - <link href="css/bootstrap-responsive.min.css" rel="stylesheet" type="text/css" media="screen" /> - <script type="text/javascript" src="js/jquery.js"></script> - <script type="text/javascript" src="js/bootstrap.min.js"></script> + <link href="{{lodspk.home}}css/bootstrap-responsive.min.css" rel="stylesheet" type="text/css" media="screen" /> + <script type="text/javascript" src="{{lodspk.home}}js/jquery.js"></script> + <script type="text/javascript" src="{{lodspk.home}}js/bootstrap.min.js"></script> + <script type="text/javascript" src="{{lodspk.home}}js/bootstrap-typeahead.js"></script> + <script type="text/javascript"> + $(document).ready(function(){ + $('.typeahead').typeahead({ + source: function (typeahead, query) { + $('.typeahead').addClass('wait');[] + return $.get('{{lodspk.home}}search/'+encodeURIComponent(query), { }, function (data) { + $('.typeahead').removeClass('wait');[] + return typeahead.process(data); + }, 'json'); + }, + onselect: function (obj) { + $('.typeahead').attr('disabled', true); + window.location = obj.uri; + } + }); + }); + </script> </head> <body> <div class="navbar navbar-fixed-top"> @@ -28,10 +52,13 @@ <a class="brand" href="{{lodspk.home}}">LODSPeaKr</a> <div class="nav-collapse"> <ul class="nav"> - <li><a href="{{lodspk.home}}">Home</a></li> - <li class="active"><a href="classes">Classes</a></li> - <li><a href="namedGraphs">Named Graphs</a></li> + <li class="active"><a href="{{lodspk.home}}">Home</a></li> + <li><a href="{{lodspk.home}}classes">Classes</a></li> + <li><a href="{{lodspk.home}}namedGraphs">Named Graphs</a></li> </ul> + <form class="navbar-search pull-left" action=""> + <input type="text" data-provide="typeahead" class="typeahead search-query span2" placeholder="Search"/> + </form> </div><!--/.nav-collapse --> </div> </div> diff --git a/doc/examples/originalComponents/services/instances/html.template b/doc/examples/originalComponents/services/instances/html.template index af6ecc44..8b40cdd3 100644 --- a/doc/examples/originalComponents/services/instances/html.template +++ b/doc/examples/originalComponents/services/instances/html.template @@ -2,7 +2,7 @@ <html lang="en"> <head> <meta charset="utf-8"> - <title>Instances of class {{lodspk.args.arg0}}</title> + <title>LODSPeaKr Basic Menu</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="description" content=""> <meta name="author" content=""> @@ -11,13 +11,37 @@ body { padding-top: 60px; /* 60px to make the container go all the way to the bottom of the topbar */ } + .wait{ + background-image:url('{{lodspk.home}}img/wait.gif'); + background-repeat:no-repeat; + padding-right:20px; + background-position: right; + } </style> <link href="{{lodspk.home}}css/bootstrap-responsive.min.css" rel="stylesheet" type="text/css" media="screen" /> <script type="text/javascript" src="{{lodspk.home}}js/jquery.js"></script> <script type="text/javascript" src="{{lodspk.home}}js/bootstrap.min.js"></script> + <script type="text/javascript" src="{{lodspk.home}}js/bootstrap-typeahead.js"></script> + <script type="text/javascript"> + $(document).ready(function(){ + $('.typeahead').typeahead({ + source: function (typeahead, query) { + $('.typeahead').addClass('wait');[] + return $.get('{{lodspk.home}}search/'+encodeURIComponent(query), { }, function (data) { + $('.typeahead').removeClass('wait');[] + return typeahead.process(data); + }, 'json'); + }, + onselect: function (obj) { + $('.typeahead').attr('disabled', true); + window.location = obj.uri; + } + }); + }); + </script> </head> <body> -<div class="navbar navbar-fixed-top"> + <div class="navbar navbar-fixed-top"> <div class="navbar-inner"> <div class="container"> <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse"> @@ -25,13 +49,16 @@ <span class="icon-bar"></span> <span class="icon-bar"></span> </a> - <a class="brand" href="#">LODSPeaKr</a> + <a class="brand" href="{{lodspk.home}}">LODSPeaKr</a> <div class="nav-collapse"> <ul class="nav"> - <li class="active"><a href="#">Home</a></li> - <li><a href="classes">Classes</a></li> - <li><a href="namedGraphs">Named Graphs</a></li> + <li class="active"><a href="{{lodspk.home}}">Home</a></li> + <li><a href="{{lodspk.home}}classes">Classes</a></li> + <li><a href="{{lodspk.home}}namedGraphs">Named Graphs</a></li> </ul> + <form class="navbar-search pull-left" action=""> + <input type="text" data-provide="typeahead" class="typeahead search-query span2" placeholder="Search"/> + </form> </div><!--/.nav-collapse --> </div> </div> diff --git a/doc/examples/originalComponents/services/namedGraphs/html.template b/doc/examples/originalComponents/services/namedGraphs/html.template index 0c411825..a00c6536 100644 --- a/doc/examples/originalComponents/services/namedGraphs/html.template +++ b/doc/examples/originalComponents/services/namedGraphs/html.template @@ -2,19 +2,43 @@ <html lang="en"> <head> <meta charset="utf-8"> - <title>LODSPeaKr: Named graphs</title> + <title>LODSPeaKr Basic Menu</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="description" content=""> <meta name="author" content=""> - <link href="css/bootstrap.min.css" rel="stylesheet" type="text/css" media="screen" /> + <link href="{{lodspk.home}}css/bootstrap.min.css" rel="stylesheet" type="text/css" media="screen" /> <style> body { padding-top: 60px; /* 60px to make the container go all the way to the bottom of the topbar */ } + .wait{ + background-image:url('{{lodspk.home}}img/wait.gif'); + background-repeat:no-repeat; + padding-right:20px; + background-position: right; + } </style> - <link href="css/bootstrap-responsive.min.css" rel="stylesheet" type="text/css" media="screen" /> - <script type="text/javascript" src="js/jquery.js"></script> - <script type="text/javascript" src="js/bootstrap.min.js"></script> + <link href="{{lodspk.home}}css/bootstrap-responsive.min.css" rel="stylesheet" type="text/css" media="screen" /> + <script type="text/javascript" src="{{lodspk.home}}js/jquery.js"></script> + <script type="text/javascript" src="{{lodspk.home}}js/bootstrap.min.js"></script> + <script type="text/javascript" src="{{lodspk.home}}js/bootstrap-typeahead.js"></script> + <script type="text/javascript"> + $(document).ready(function(){ + $('.typeahead').typeahead({ + source: function (typeahead, query) { + $('.typeahead').addClass('wait');[] + return $.get('{{lodspk.home}}search/'+encodeURIComponent(query), { }, function (data) { + $('.typeahead').removeClass('wait');[] + return typeahead.process(data); + }, 'json'); + }, + onselect: function (obj) { + $('.typeahead').attr('disabled', true); + window.location = obj.uri; + } + }); + }); + </script> </head> <body> <div class="navbar navbar-fixed-top"> @@ -28,10 +52,13 @@ <a class="brand" href="{{lodspk.home}}">LODSPeaKr</a> <div class="nav-collapse"> <ul class="nav"> - <li><a href="{{lodspk.home}}">Home</a></li> - <li><a href="classes">Classes</a></li> - <li class="active"><a href="namedGraphs">Named Graphs</a></li> + <li class="active"><a href="{{lodspk.home}}">Home</a></li> + <li><a href="{{lodspk.home}}classes">Classes</a></li> + <li><a href="{{lodspk.home}}namedGraphs">Named Graphs</a></li> </ul> + <form class="navbar-search pull-left" action=""> + <input type="text" data-provide="typeahead" class="typeahead search-query span2" placeholder="Search"/> + </form> </div><!--/.nav-collapse --> </div> </div> diff --git a/doc/examples/originalComponents/services/search/html.template b/doc/examples/originalComponents/services/search/html.template new file mode 120000 index 00000000..8ae3a8f8 --- /dev/null +++ b/doc/examples/originalComponents/services/search/html.template @@ -0,0 +1 @@ +json.template \ No newline at end of file diff --git a/doc/examples/originalComponents/services/search/json.template b/doc/examples/originalComponents/services/search/json.template new file mode 100644 index 00000000..cc4f46fd --- /dev/null +++ b/doc/examples/originalComponents/services/search/json.template @@ -0,0 +1,5 @@ +[ + {%for i in models.main%}{%if !forloop.first && models.main|length > 1%},{%endif%} + { "value": "{{i.label.value}}", "uri": "{{i.resource.value}}"} + {%endfor%} + ] diff --git a/doc/examples/originalComponents/services/search/queries/main.query b/doc/examples/originalComponents/services/search/queries/main.query new file mode 100644 index 00000000..41e032be --- /dev/null +++ b/doc/examples/originalComponents/services/search/queries/main.query @@ -0,0 +1,11 @@ +SELECT DISTINCT ?resource ?label WHERE { + { + GRAPH ?g { + ?resource rdfs:label ?label . + } + }UNION{ + ?resource rdfs:label ?label . + } + FILTER(regex(str(?label), "{{lodspk.args.arg0}}", "i")) +} +LIMIT 10 diff --git a/doc/examples/originalComponents/types/rdfs:Resource/html.template b/doc/examples/originalComponents/types/rdfs:Resource/html.template index 23d2458b..e46be7ff 100644 --- a/doc/examples/originalComponents/types/rdfs:Resource/html.template +++ b/doc/examples/originalComponents/types/rdfs:Resource/html.template @@ -2,7 +2,7 @@ <html lang="en"> <head> <meta charset="utf-8"> - <title>Instances of class {{lodspk.args.arg0}}</title> + <title>LODSPeaKr Basic Menu</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="description" content=""> <meta name="author" content=""> @@ -11,13 +11,37 @@ body { padding-top: 60px; /* 60px to make the container go all the way to the bottom of the topbar */ } + .wait{ + background-image:url('{{lodspk.home}}img/wait.gif'); + background-repeat:no-repeat; + padding-right:20px; + background-position: right; + } </style> <link href="{{lodspk.home}}css/bootstrap-responsive.min.css" rel="stylesheet" type="text/css" media="screen" /> <script type="text/javascript" src="{{lodspk.home}}js/jquery.js"></script> <script type="text/javascript" src="{{lodspk.home}}js/bootstrap.min.js"></script> + <script type="text/javascript" src="{{lodspk.home}}js/bootstrap-typeahead.js"></script> + <script type="text/javascript"> + $(document).ready(function(){ + $('.typeahead').typeahead({ + source: function (typeahead, query) { + $('.typeahead').addClass('wait');[] + return $.get('{{lodspk.home}}search/'+encodeURIComponent(query), { }, function (data) { + $('.typeahead').removeClass('wait');[] + return typeahead.process(data); + }, 'json'); + }, + onselect: function (obj) { + $('.typeahead').attr('disabled', true); + window.location = obj.uri; + } + }); + }); + </script> </head> <body> -<div class="navbar navbar-fixed-top"> + <div class="navbar navbar-fixed-top"> <div class="navbar-inner"> <div class="container"> <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse"> @@ -28,10 +52,13 @@ <a class="brand" href="{{lodspk.home}}">LODSPeaKr</a> <div class="nav-collapse"> <ul class="nav"> - <li class="active"><a href="#">Home</a></li> - <li><a href="classes">Classes</a></li> - <li><a href="namedGraphs">Named Graphs</a></li> + <li class="active"><a href="{{lodspk.home}}">Home</a></li> + <li><a href="{{lodspk.home}}classes">Classes</a></li> + <li><a href="{{lodspk.home}}namedGraphs">Named Graphs</a></li> </ul> + <form class="navbar-search pull-left" action=""> + <input type="text" data-provide="typeahead" class="typeahead search-query span2" placeholder="Search"/> + </form> </div><!--/.nav-collapse --> </div> </div> diff --git a/static/img/wait.gif b/static/img/wait.gif new file mode 100644 index 0000000000000000000000000000000000000000..e192ca895cd00d6b752ec84619b787188f30ee41 GIT binary patch literal 847 zcmZ?wbhEHb6krfw_`<;O|Nnmm28JI$eppyow6wIav9YPCsa?BvZN-WeVq#)tWo2n; zX-}R!nK5I=v17+PJUqg}!hq_D2a5l>{aizWogD*Qjr0td8G$+#|4BI)r6!i7rYMwW zmSiX-W+hhS<R_-+W#*;jGbsLK;p76U(gE24awG$5fPz9_O6I(!1|m6%S}h5y*3J+) zHc`s7;RD;vj)v%778|C_)_{out&E+mi~81R>NI#p{aB=u^kJ9BqzM)+D@@g7D>_ZH z6>Nk>K2^#dec$hd&5{fSg)a9?JsDb3M<1+M;h^GLd*HyqYe$(ldZsj_W{3#!96X@l zAjsu&py5MupnEfu)0U^(0!(Kp*sL-QO$pql{X%Kq;`Av7E5z0<TG;QHEpT4hk%8ML zz}$e{hLfeifG?8GLW`HPU0D&E<q1F^Du14!aZAKQi|a^$2&e9ncEk6^ja*jFrwzTg zc(gP<Wb@FQp>lI$B?E`RzKdsAZ)9=nHHN!5+~JF4SY+VADb}iE(C2i8t1nx?>)BhL zP<zvIW|kV2Y~C16ex7cumIgtBwLxJDlcC5_hZW7va%^kFk7ai^&0$n@dXiwz(72sx z-vnjXCW|Vkz=($A-MNd(xP_D!D!e?jIH572jW^V7nPkx&Nxr28uQflX{cpf(4ajf6 z*jwSi<7y~y=%hq$ti<u15*|Gl+HPpD3v{uVFeyCO(2-c?rkLo#5D`3sqcPWX$yUZB z7mqD$46Ak~Ch2hXsWucCizOdX-k|=2_l**}iJ5tTnpT8<gGNuU3RX+wp_Y08J;t%j z!Gz(^relsuo)bAX%vzYhVVBp+>S>_TA<--6ZN7=uLx=rfr*28JR#UU9l!(BR!@3s} qR&*pBVEQRw*vTQWVY)*<pR;)ehrS_88f%PZcaNGPgEc65f&l>bLIMx~ literal 0 HcmV?d00001 diff --git a/static/index.html b/static/index.html index e99e8429..22e4cd5f 100644 --- a/static/index.html +++ b/static/index.html @@ -11,10 +11,34 @@ body { padding-top: 60px; /* 60px to make the container go all the way to the bottom of the topbar */ } + .wait{ + background-image:url('img/wait.gif'); + background-repeat:no-repeat; + padding-right:20px; + background-position: right; + } </style> <link href="css/bootstrap-responsive.min.css" rel="stylesheet" type="text/css" media="screen" /> <script type="text/javascript" src="js/jquery.js"></script> <script type="text/javascript" src="js/bootstrap.min.js"></script> + <script type="text/javascript" src="js/bootstrap-typeahead.js"></script> + <script type="text/javascript"> + $(document).ready(function(){ + $('.typeahead').typeahead({ + source: function (typeahead, query) { + $('.typeahead').addClass('wait');[] + return $.get('search/'+encodeURIComponent(query), { }, function (data) { + $('.typeahead').removeClass('wait');[] + return typeahead.process(data); + }, 'json'); + }, + onselect: function (obj) { + $('.typeahead').attr('disabled', true); + window.location = obj.uri; + } + }); + }); + </script> </head> <body> <div class="navbar navbar-fixed-top"> @@ -32,6 +56,9 @@ <li><a href="classes">Classes</a></li> <li><a href="namedGraphs">Named Graphs</a></li> </ul> + <form class="navbar-search pull-left" action=""> + <input type="text" data-provide="typeahead" class="typeahead search-query span2" placeholder="Search"/> + </form> </div><!--/.nav-collapse --> </div> </div> @@ -51,7 +78,7 @@ </div> <div class="span5 well"> <h2>Understanding components</h2> - <p>In LODSPeaKr, components are entities (usually a set of templates and sparql queries) that serve are applied when a particular URI is being retrieved. The templates are based on <a href='http://haanga.org/'>Haanga</a>, and you can see <a href='https://github.com/alangrafu/lodspeakr/wiki/Examples-how-to-use-haanga-in-lodspeakr'>several examples of Haanga in LODSPeaKr in the wiki</a>.</p> + <p>In LODSPeaKr, components are entities (usually a set of templates and sparql queries) that are applied when a particular URI is being retrieved. The templates are based on <a href='http://haanga.org/'>Haanga</a>, and you can see <a href='https://github.com/alangrafu/lodspeakr/wiki/Examples-how-to-use-haanga-in-lodspeakr'>several examples of Haanga in LODSPeaKr in the wiki</a>.</p> <p>Currently, LODSPeaKr supports 3 types of components. You can learn <a href='https://github.com/alangrafu/lodspeakr/wiki/Creating-components-in-LODSPeaKr'>how to create components</a> in the wiki.</p> <ul> <li><strong>Type components:</strong> Allow you to define how LODSPeaKr should expose data for instances of a certain class (for example all instances of <code>foaf:Person</code>).</li> diff --git a/static/js/bootstrap-typeahead.js b/static/js/bootstrap-typeahead.js new file mode 100644 index 00000000..dd58c66b --- /dev/null +++ b/static/js/bootstrap-typeahead.js @@ -0,0 +1,311 @@ +/* ============================================================= + * bootstrap-typeahead.js v2.0.0 + * http://twitter.github.com/bootstrap/javascript.html#typeahead + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + +!function( $ ){ + + "use strict" + + var Typeahead = function ( element, options ) { + this.$element = $(element) + this.options = $.extend({}, $.fn.typeahead.defaults, options) + this.matcher = this.options.matcher || this.matcher + this.sorter = this.options.sorter || this.sorter + this.highlighter = this.options.highlighter || this.highlighter + this.$menu = $(this.options.menu).appendTo('body') + this.source = this.options.source + this.onselect = this.options.onselect + this.strings = true + this.shown = false + this.listen() + } + + Typeahead.prototype = { + + constructor: Typeahead + + , select: function () { + var val = JSON.parse(this.$menu.find('.active').attr('data-value')) + , text + + if (!this.strings) text = val[this.options.property] + else text = val + + this.$element.val(text) + + if (typeof this.onselect == "function") + this.onselect(val) + + return this.hide() + } + + , show: function () { + var pos = $.extend({}, this.$element.offset(), { + height: this.$element[0].offsetHeight + }) + + this.$menu.css({ + top: pos.top + pos.height + , left: pos.left + }) + + this.$menu.show() + this.shown = true + return this + } + + , hide: function () { + this.$menu.hide() + this.shown = false + return this + } + + , lookup: function (event) { + var that = this + , items + , q + , value + + this.query = this.$element.val() + + if (typeof this.source == "function") { + value = this.source(this, this.query) + if (value) this.process(value) + } else { + this.process(this.source) + } + } + + , process: function (results) { + var that = this + , items + , q + + if (results.length && typeof results[0] != "string") + this.strings = false + + this.query = this.$element.val() + + if (!this.query) { + return this.shown ? this.hide() : this + } + + items = $.grep(results, function (item) { + if (!that.strings) + item = item[that.options.property] + if (that.matcher(item)) return item + }) + + items = this.sorter(items) + + if (!items.length) { + return this.shown ? this.hide() : this + } + + return this.render(items.slice(0, this.options.items)).show() + } + + , matcher: function (item) { + return ~item.toLowerCase().indexOf(this.query.toLowerCase()) + } + + , sorter: function (items) { + var beginswith = [] + , caseSensitive = [] + , caseInsensitive = [] + , item + , sortby + + while (item = items.shift()) { + if (this.strings) sortby = item + else sortby = item[this.options.property] + + if (!sortby.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item) + else if (~sortby.indexOf(this.query)) caseSensitive.push(item) + else caseInsensitive.push(item) + } + + return beginswith.concat(caseSensitive, caseInsensitive) + } + + , highlighter: function (item) { + return item.replace(new RegExp('(' + this.query + ')', 'ig'), function ($1, match) { + return '<strong>' + match + '</strong>' + }) + } + + , render: function (items) { + var that = this + + items = $(items).map(function (i, item) { + i = $(that.options.item).attr('data-value', JSON.stringify(item)) + if (!that.strings) + item = item[that.options.property] + i.find('a').html(that.highlighter(item)) + return i[0] + }) + + items.first().addClass('active') + this.$menu.html(items) + return this + } + + , next: function (event) { + var active = this.$menu.find('.active').removeClass('active') + , next = active.next() + + if (!next.length) { + next = $(this.$menu.find('li')[0]) + } + + next.addClass('active') + } + + , prev: function (event) { + var active = this.$menu.find('.active').removeClass('active') + , prev = active.prev() + + if (!prev.length) { + prev = this.$menu.find('li').last() + } + + prev.addClass('active') + } + + , listen: function () { + this.$element + .on('blur', $.proxy(this.blur, this)) + .on('keypress', $.proxy(this.keypress, this)) + .on('keyup', $.proxy(this.keyup, this)) + + if ($.browser.webkit || $.browser.msie) { + this.$element.on('keydown', $.proxy(this.keypress, this)) + } + + this.$menu + .on('click', $.proxy(this.click, this)) + .on('mouseenter', 'li', $.proxy(this.mouseenter, this)) + } + + , keyup: function (e) { + e.stopPropagation() + e.preventDefault() + + switch(e.keyCode) { + case 40: // down arrow + case 38: // up arrow + break + + case 9: // tab + case 13: // enter + if (!this.shown) return + this.select() + break + + case 27: // escape + this.hide() + break + + default: + this.lookup() + } + + } + + , keypress: function (e) { + e.stopPropagation() + if (!this.shown) return + + switch(e.keyCode) { + case 9: // tab + case 13: // enter + case 27: // escape + e.preventDefault() + break + + case 38: // up arrow + e.preventDefault() + this.prev() + break + + case 40: // down arrow + e.preventDefault() + this.next() + break + } + } + + , blur: function (e) { + var that = this + e.stopPropagation() + e.preventDefault() + setTimeout(function () { that.hide() }, 150) + } + + , click: function (e) { + e.stopPropagation() + e.preventDefault() + this.select() + } + + , mouseenter: function (e) { + this.$menu.find('.active').removeClass('active') + $(e.currentTarget).addClass('active') + } + + } + + + /* TYPEAHEAD PLUGIN DEFINITION + * =========================== */ + + $.fn.typeahead = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('typeahead') + , options = typeof option == 'object' && option + if (!data) $this.data('typeahead', (data = new Typeahead(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.typeahead.defaults = { + source: [] + , items: 8 + , menu: '<ul class="typeahead dropdown-menu"></ul>' + , item: '<li><a href="#"></a></li>' + , onselect: null + , property: 'value' + } + + $.fn.typeahead.Constructor = Typeahead + + + /* TYPEAHEAD DATA-API + * ================== */ + + $(function () { + $('body').on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) { + var $this = $(this) + if ($this.data('typeahead')) return + e.preventDefault() + $this.typeahead($this.data()) + }) + }) + +}( window.jQuery ); diff --git a/static/js/typeahead.js b/static/js/typeahead.js new file mode 100644 index 00000000..bd044a81 --- /dev/null +++ b/static/js/typeahead.js @@ -0,0 +1,387 @@ +/* ============================================================= + * bootstrap-typeahead.js v2.0.3 + * http://twitter.github.com/bootstrap/javascript.html#typeahead + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + +/* + * + * Modifications by Paul Warelis + * + */ + +!function($){ + + "use strict"; // jshint ;_; + + /* TYPEAHEAD PUBLIC CLASS DEFINITION + * ================================= */ + + var Typeahead = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.typeahead.defaults, options) + this.matcher = this.options.matcher || this.matcher + this.sorter = this.options.sorter || this.sorter + this.highlighter = this.options.highlighter || this.highlighter + this.updater = this.options.updater || this.updater + this.$menu = $(this.options.menu).appendTo('body') + if (this.options.ajax) { + var ajax = this.options.ajax; + if (typeof ajax == "string") { + ajax = { url:ajax }; + } + this.ajax = { + url : ajax.url, + timeout : ajax.timeout || 300, + method: ajax.method || "post", + triggerLength : ajax.triggerLength || 3, + loadingClass : ajax.loadingClass || null, + displayField : ajax.displayField || null, + preDispatch : ajax.preDispatch || null, + preProcess : ajax.preProcess || null + } + this.query = ""; + } else { + this.source = this.options.source + this.ajax = null; + } + this.shown = false + this.listen() + } + + Typeahead.prototype = { + + constructor: Typeahead, + + select: function () { + var val = this.$menu.find('.active').attr('data-value') + this.$element + .val(this.updater(val)) + .change() + return this.hide() + }, + + updater: function (item) { + return item + }, + + show: function () { + var pos = $.extend({}, this.$element.offset(), { + height: this.$element[0].offsetHeight + }) + + this.$menu.css({ + top: pos.top + pos.height, + left: pos.left + }) + + this.$menu.show() + this.shown = true + return this + }, + + hide: function () { + this.$menu.hide() + this.shown = false + return this + }, + + ajaxLookup: function () { + + var query = this.$element.val(); + + if (query == this.query) { + return this; + } + + // Query changed + this.query = query + + // Cancel last timer if set + if (this.ajax.timerId) { + clearTimeout(this.ajax.timerId); + this.ajax.timerId = null; + } + + if (!query || query.length < this.ajax.triggerLength) { + // cancel the ajax callback if in progress + if (this.ajax.xhr) { + this.ajax.xhr.abort(); + this.ajax.xhr = null; + this.ajaxToggleLoadClass(false); + } + return this.shown ? this.hide() : this + } + + function execute() { + this.ajaxToggleLoadClass(true); + + // Cancel last call if already in progress + if (this.ajax.xhr) this.ajax.xhr.abort(); + + var params = this.ajax.preDispatch ? this.ajax.preDispatch(query) : { query : query } + var jAjax = (this.ajax.method == "post") ? $.post : $.get; + this.ajax.xhr = jAjax(this.ajax.url, params, $.proxy(this.ajaxSource, this)); + this.ajax.timerId = null; + } + + // Query is good to send, set a timer + this.ajax.timerId = setTimeout($.proxy(execute, this), this.ajax.timeout); + + return this; + }, + + ajaxSource: function (data) { + this.ajaxToggleLoadClass(false); + + var that = this, items + + if (!this.ajax.xhr) return; + + if (this.ajax.preProcess) { + data = this.ajax.preProcess(data); + } + // Save for selection retreival + this.ajax.data = data; + + items = $.grep(data, function (item) { + if (that.ajax.displayField) { + item = item[that.ajax.displayField] + } + if (that.matcher(item)) return item + }) + + items = this.sorter(items) + + if (!items.length) { + return this.shown ? this.hide() : this + } + + this.ajax.xhr = null; + return this.render(items.slice(0, this.options.items)).show() + }, + + ajaxToggleLoadClass: function (enable) { + if (!this.ajax.loadingClass) return; + this.$element.toggleClass(this.ajax.loadingClass, enable); + }, + + lookup: function (event) { + var that = this, items + + this.query = this.$element.val() + + if (!this.query) { + return this.shown ? this.hide() : this + } + + items = $.grep(this.source, function (item) { + return that.matcher(item) + }) + + items = this.sorter(items) + + if (!items.length) { + return this.shown ? this.hide() : this + } + + return this.render(items.slice(0, this.options.items)).show() + }, + + matcher: function (item) { + return ~item.toLowerCase().indexOf(this.query.toLowerCase()) + }, + + sorter: function (items) { + var beginswith = [], + caseSensitive = [], + caseInsensitive = [], + item + + while (item = items.shift()) { + if (this.ajax && this.ajax.displayField) { + item = item[this.ajax.displayField] + } + if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item) + else if (~item.indexOf(this.query)) caseSensitive.push(item) + else caseInsensitive.push(item) + } + + return beginswith.concat(caseSensitive, caseInsensitive) + }, + + highlighter: function (item) { + var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&') + return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) { + return '<strong>' + match + '</strong>' + }) + }, + + render: function (items) { + var that = this + + items = $(items).map(function (i, item) { + i = $(that.options.item).attr('data-value', item) + i.find('a').html(that.highlighter(item)) + return i[0] + }) + + items.first().addClass('active') + this.$menu.html(items) + return this + }, + + next: function (event) { + var active = this.$menu.find('.active').removeClass('active'), + next = active.next() + + if (!next.length) { + next = $(this.$menu.find('li')[0]) + } + + next.addClass('active') + }, + + prev: function (event) { + var active = this.$menu.find('.active').removeClass('active'), + prev = active.prev() + + if (!prev.length) { + prev = this.$menu.find('li').last() + } + + prev.addClass('active') + }, + + listen: function () { + this.$element + .on('blur', $.proxy(this.blur, this)) + .on('keypress', $.proxy(this.keypress, this)) + .on('keyup', $.proxy(this.keyup, this)) + + // Firefox needs this too + this.$element.on('keydown', $.proxy(this.keypress, this)) + + this.$menu + .on('click', $.proxy(this.click, this)) + .on('mouseenter', 'li', $.proxy(this.mouseenter, this)) + }, + + keyup: function (e) { + switch(e.keyCode) { + case 40: // down arrow + case 38: // up arrow + break + + case 9: // tab + case 13: // enter + if (!this.shown) return + this.select() + break + + case 27: // escape + if (!this.shown) return + this.hide() + break + + default: + if (this.ajax) this.ajaxLookup() + else this.lookup() + } + + e.stopPropagation() + e.preventDefault() + }, + + keypress: function (e) { + if (!this.shown) return + + switch(e.keyCode) { + case 9: // tab + case 13: // enter + case 27: // escape + e.preventDefault() + break + + case 38: // up arrow + if (e.type != 'keydown') break + e.preventDefault() + this.prev() + break + + case 40: // down arrow + if (e.type != 'keydown') break + e.preventDefault() + this.next() + break + } + + e.stopPropagation() + }, + + blur: function (e) { + var that = this + setTimeout(function () { that.hide() }, 150) + }, + + click: function (e) { + e.stopPropagation() + e.preventDefault() + this.select() + }, + + mouseenter: function (e) { + this.$menu.find('.active').removeClass('active') + $(e.currentTarget).addClass('active') + } + } + + + /* TYPEAHEAD PLUGIN DEFINITION + * =========================== */ + + $.fn.typeahead = function (option) { + return this.each(function () { + var $this = $(this), + data = $this.data('typeahead'), + options = typeof option == 'object' && option + if (!data) $this.data('typeahead', (data = new Typeahead(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.typeahead.defaults = { + source: [], + items: 8, + menu: '<ul class="typeahead dropdown-menu"></ul>', + item: '<li><a href="#"></a></li>' + } + + $.fn.typeahead.Constructor = Typeahead + + /* TYPEAHEAD DATA-API + * ================== */ + + $(function () { + $('body').on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) { + var $this = $(this) + if ($this.data('typeahead')) return + e.preventDefault() + $this.typeahead($this.data()) + }) + }) + +}(window.jQuery); -- GitLab