/** * A jQuery plugin to add typeahead search functionality to the navbar search * box. This requires Hogan for templating and typeahead.js for the actual * typeahead functionality. */ (function ($) { /** * A backend, which encapsulates a set of completions, such as a list of * functions or classes. * * @constructor * @param {String} label The label to show the user. */ var Backend = function (label) { this.label = label; this.elements = {}; }; /** * Adds an item to the backend. * * @param {String} id The item ID. It would help if this was unique. * @param {String} name The item name to use as a label. * @param {Array} tokens An array of tokens that should match this item. */ Backend.prototype.addItem = function (id, name, description, tokens) { this.elements[id] = { tokens: tokens, id: id, name: name, description: description }; }; /** * Returns the backend contents formatted as an array that typeahead.js can * digest as a local data source. * * @return {Array} */ Backend.prototype.toTypeaheadArray = function () { var array = []; $.each(this.elements, function (_, element) { array.push(element); }); /* This is a rather convoluted sorting function, but the idea is to * make the results as useful as possible, since only a few are shown * at any one time. In general, we favour shorter names over longer * ones, and favour regular functions over methods when sorting * functions. Ideally, this would actually sort based on function * popularity, but this is a simpler algorithmic approach for now that * seems to result in generally useful results. */ array.sort(function (a, b) { var a = a.name; var b = b.name; var aIsMethod = (a.indexOf("::") != -1); var bIsMethod = (b.indexOf("::") != -1); // Methods are always after regular functions. if (aIsMethod && !bIsMethod) { return 1; } else if (bIsMethod && !aIsMethod) { return -1; } /* If one function name is the exact prefix of the other, we want * to sort the shorter version first (mostly for things like date() * versus date_format()). */ if (a.length > b.length) { if (a.indexOf(b) == 0) { return 1; } } else { if (b.indexOf(a) == 0) { return -1; } } // Otherwise, sort normally. if (a > b) { return 1; } else if (a < b) { return -1; } return 0; }); return array; }; /** * The actual search plugin. Should be applied to the input that needs * typeahead functionality. * * @param {Object} options The options object. This should include * "language": the language to try to load, * "limit": the maximum number of results */ $.fn.search = function (options) { var element = this; options.language = options.language || "en"; options.limit = options.limit || 30; /** * Utility function to check if the user's browser supports local * storage and native JSON, in which case we'll use it to cache the * search JSON. * * @return {Boolean} */ var canCache = function () { try { return ('localStorage' in window && window['localStorage'] !== null && "JSON" in window && window["JSON"] !== null); } catch (e) { return false; } }; /** * Processes a data structure in the format of our search-index.php * files and returns an object containing multiple Backend objects. * * @param {Object} index * @return {Object} */ var processIndex = function (index) { // The search types we want to support. var backends = { "function": new Backend("Functions"), "variable": new Backend("Variables"), "class": new Backend("Classes"), "exception": new Backend("Exceptions"), "extension": new Backend("Extensions"), "general": new Backend("Other Matches") }; $.each(index, function (id, item) { /* If the item has a name, then we should figure out what type * of data this is, and hence which backend this should go * into. */ if (item[0]) { var tokens = [item[0]]; var type = null; if (item[0].indexOf("_") != -1) { tokens.push(item[0].replace("_", "")); } if (item[0].indexOf("::") != -1) { /* We'll add tokens to make the autocompletion more * useful: users can search for method names and can * specify that they only want method names by * prefixing their search with ::. */ tokens.push(item[0].split("::")[1]); tokens.push("::" + item[0].split("::")[1]); } switch(item[2]) { case "phpdoc:varentry": type = "variable"; break; case "refentry": type = "function"; break; case "phpdoc:exceptionref": type = "exception"; break; case "phpdoc:classref": type = "class"; break; case "set": case "book": case "reference": type = "extension"; break; case "section": case "chapter": case "appendix": case "article": default: type = "general"; } if (type) { backends[type].addItem(id, item[0], item[1], tokens); } } }); return backends; }; /** * Attempt to asynchronously load the search JSON for a given language. * * @param {String} language The language to search for. * @param {Function} success Success handler, which will be given an * object containing multiple Backend * objects on success. * @param {Function} failure An optional failure handler. */ var loadLanguage = function (language, success, failure) { var key = "search-" + language; // Check if the cache has a recent enough search index. if (canCache()) { var cache = window.localStorage.getItem(key); if (cache) { var since = new Date(); // Parse the stored JSON. cache = JSON.parse(cache); // We'll use anything that's less than two weeks old. since.setDate(since.getDate() - 14); if (cache.time > since.getTime()) { success($.map(cache.data, function (dataset, name) { // Rehydrate the Backend objects. var backend = new Backend(dataset.label); backend.elements = dataset.elements; return backend; })); return; } } } // OK, nothing cached. $.ajax({ dataType: "json", error: failure, success: function (data) { // Transform the data into something useful. var backends = processIndex(data); // Cache the data if we can. if (canCache()) { /* This may fail in IE 8 due to exceeding the local * storage limit. If so, squash the exception: this * isn't a required part of the system. */ try { window.localStorage.setItem(key, JSON.stringify({ data: backends, time: new Date().getTime() }) ); } catch (e) { // Derp. } } success(backends); }, url: "/js/search-index.php?lang=" + language }); }; /** * Actually enables the typeahead on the DOM element. * * @param {Object} backends An array-like object containing backends. */ var enableSearchTypeahead = function (backends) { var template = "