--- web/html/home.php | 19 ++- web/html/index.php | 4 + web/html/js/bootstrap-typeahead.js | 311 +++++++++++++++++++++++++++++++++++++ web/html/suggest.php | 12 ++ web/lib/routing.inc.php | 1 + web/lib/suggestfunc.inc.php | 32 ++++ 6 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 web/html/js/bootstrap-typeahead.js create mode 100644 web/html/suggest.php create mode 100644 web/lib/suggestfunc.inc.php diff --git a/web/html/home.php b/web/html/home.php index 4e489ba..2d6d786 100644 --- a/web/html/home.php +++ b/web/html/home.php @@ -95,7 +95,7 @@ $dbh = db_connect(); <fieldset> <label for="pkgsearch-field"><?= __('Package Search') ?>:</label> <input type="hidden" name="O" value="0" /> - <input type="text" name="K" size="30" value="<?php if (isset($_REQUEST["K"])) { print stripslashes(trim(htmlspecialchars($_REQUEST["K"], ENT_QUOTES))); } ?>" maxlength="35" /> + <input id="pkgsearch-field" type="text" name="K" size="30" value="<?php if (isset($_REQUEST["K"])) { print stripslashes(trim(htmlspecialchars($_REQUEST["K"], ENT_QUOTES))); } ?>" maxlength="35" /> </fieldset> </form> </div> @@ -107,5 +107,22 @@ $dbh = db_connect(); </div> </div> +<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script> +<script type="text/javascript" src="/js/bootstrap-typeahead.js"></script> +<script type="text/javascript"> +$(document).ready(function() { + $('#pkgsearch-field').typeahead({ + source: function(query, callback) { + $.getJSON('/suggest', {q: query}, function(data) { + callback(data); + }); + }, + matcher: function(item) { return true; }, + sorter: function(items) { return items; }, + menu: '<ul class="pkgsearch-typeahead"></ul>', + items: 20 + }).attr('autocomplete', 'off'); +}); +</script> <?php html_footer(AUR_VERSION); diff --git a/web/html/index.php b/web/html/index.php index a197d0b..c51f409 100644 --- a/web/html/index.php +++ b/web/html/index.php @@ -118,6 +118,10 @@ if (!empty($tokens[1]) && '/' . $tokens[1] == get_pkg_route()) { header("Content-Type: image/png"); include "./$path"; break; + case "/js/bootstrap-typeahead.js": + header("Content-Type: application/javascript"); + include "./$path"; + break; default: header("HTTP/1.0 404 Not Found"); include "./404.php"; diff --git a/web/html/js/bootstrap-typeahead.js b/web/html/js/bootstrap-typeahead.js new file mode 100644 index 0000000..2bb0355 --- /dev/null +++ b/web/html/js/bootstrap-typeahead.js @@ -0,0 +1,311 @@ +/* ============================================================= + * bootstrap-typeahead.js v2.2.1 + * 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"; // 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') + this.source = this.options.source + 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 + } + + , lookup: function (event) { + var items + + this.query = this.$element.val() + + if (!this.query || this.query.length < this.options.minLength) { + return this.shown ? this.hide() : this + } + + items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source + + return items ? this.process(items) : this + } + + , process: function (items) { + var that = this + + items = $.grep(items, 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 (!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)) + + if (this.eventSupported('keydown')) { + this.$element.on('keydown', $.proxy(this.keydown, this)) + } + + this.$menu + .on('click', $.proxy(this.click, this)) + .on('mouseenter', 'li', $.proxy(this.mouseenter, this)) + } + + , eventSupported: function(eventName) { + var isSupported = eventName in this.$element + if (!isSupported) { + this.$element.setAttribute(eventName, 'return;') + isSupported = typeof this.$element[eventName] === 'function' + } + return isSupported + } + + , move: 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 + e.preventDefault() + this.prev() + break + + case 40: // down arrow + e.preventDefault() + this.next() + break + } + + e.stopPropagation() + } + + , keydown: function (e) { + this.suppressKeyPressRepeat = !~$.inArray(e.keyCode, [40,38,9,13,27]) + this.move(e) + } + + , keypress: function (e) { + if (this.suppressKeyPressRepeat) return + this.move(e) + } + + , keyup: function (e) { + switch(e.keyCode) { + case 40: // down arrow + case 38: // up arrow + case 16: // shift + case 17: // ctrl + case 18: // alt + 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: + this.lookup() + } + + e.stopPropagation() + e.preventDefault() + } + + , blur: function (e) { + var that = this + setTimeout(function () { that.hide() }, 150) + } + + , click: function (e) { + e.stopPropagation() + e.preventDefault() + this.select() + this.$element.focus() + } + + , 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></a></li>' + , minLength: 1 + } + + $.fn.typeahead.Constructor = Typeahead + + + /* TYPEAHEAD DATA-API + * ================== */ + + $(document).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/web/html/suggest.php b/web/html/suggest.php new file mode 100644 index 0000000..2696662 --- /dev/null +++ b/web/html/suggest.php @@ -0,0 +1,12 @@ +<?php +set_include_path(get_include_path() . PATH_SEPARATOR . '../lib'); +include_once('suggestfunc.inc.php'); + +if ( $_SERVER['REQUEST_METHOD'] != 'GET' ) { + header('HTTP/1.1 405 Method Not Allowed'); + exit(); +} + +if ( isset($_GET['q']) ) { + echo suggest_search($_GET['q']); +} diff --git a/web/lib/routing.inc.php b/web/lib/routing.inc.php index 206886c..ebe9dd3 100644 --- a/web/lib/routing.inc.php +++ b/web/lib/routing.inc.php @@ -13,6 +13,7 @@ $ROUTES = array( '/rpc' => 'rpc.php', '/rss' => 'rss.php', '/submit' => 'pkgsubmit.php', + '/suggest' => 'suggest.php', '/tu' => 'tu.php', '/addvote' => 'addvote.php', ); diff --git a/web/lib/suggestfunc.inc.php b/web/lib/suggestfunc.inc.php new file mode 100644 index 0000000..7519da9 --- /dev/null +++ b/web/lib/suggestfunc.inc.php @@ -0,0 +1,32 @@ +<?php +/** + * This file contains the typeahead suggest search function + **/ +include_once("aur.inc.php"); + +/** + * This function searches the package database + * @param string $search Contains search string + * @param \PDO $dbh Already established database connection + * @return string The JSON formatted response data + **/ +function suggest_search($search="", $dbh=NULL) { + if(!$dbh) { + $dbh = db_connect(); + } + + // get all package names that start with $search + $query = 'SELECT Name FROM Packages WHERE Name LIKE ' . + $dbh->quote(addcslashes($search, '%_') . '%') . + ' ORDER BY Name ASC LIMIT 20'; + + $result = $dbh->query($query); + $json_array = array(); + + if ($result) { + $json_array = $result->fetchAll(PDO::FETCH_COLUMN, 0); + } + + header('Content-Type: application/json'); + return json_encode($json_array); +} -- 1.8.0.2