| // © 2009–2010 EPFL/LAMP |
| // code by Gilles Dubochet with contributions by Johannes Rudolph, "spiros", Marcin Kubala and Felix Mulder |
| |
| var scheduler = undefined; |
| |
| var title = $(document).attr('title'); |
| |
| var lastFragment = ""; |
| |
| var Index = {}; |
| (function (ns) { |
| ns.keyLength = 0; |
| ns.keys = function (obj) { |
| var result = []; |
| var key; |
| for (key in obj) { |
| result.push(key); |
| ns.keyLength++; |
| } |
| return result; |
| } |
| })(Index); |
| |
| /** Find query string from URL */ |
| var QueryString = function(key) { |
| if (QueryString.map === undefined) { // only calc once |
| QueryString.map = {}; |
| var keyVals = window.location.search.split("?").pop().split("&"); |
| keyVals.forEach(function(elem) { |
| var pair = elem.split("="); |
| if (pair.length == 2) QueryString.map[pair[0]] = pair[1]; |
| }); |
| } |
| |
| return QueryString.map[key]; |
| }; |
| |
| $(document).ready(function() { |
| // Clicking #doc-title returns the user to the root package |
| $("#doc-title").on("click", function() { document.location = toRoot + "index.html" }); |
| |
| scheduler = new Scheduler(); |
| scheduler.addLabel("init", 1); |
| scheduler.addLabel("focus", 2); |
| scheduler.addLabel("filter", 4); |
| scheduler.addLabel("search", 5); |
| |
| configureTextFilter(); |
| |
| $("#index-input").on("input", function(e) { |
| if($(this).val().length > 0) |
| $("#textfilter > .input > .clear").show(); |
| else |
| $("#textfilter > .input > .clear").hide(); |
| }); |
| |
| if (QueryString("search") !== undefined) { |
| $("#index-input").val(QueryString("search")); |
| searchAll(); |
| } |
| }); |
| |
| /* Handles all key presses while scrolling around with keyboard shortcuts in search results */ |
| function handleKeyNavigation() { |
| /** Iterates both back and forth among selected elements */ |
| var EntityIterator = function (litems, ritems) { |
| var it = this; |
| this.index = -1; |
| |
| this.items = litems; |
| this.litems = litems; |
| this.ritems = ritems; |
| |
| if (litems.length == 0) |
| this.items = ritems; |
| |
| /** Returns the next entry - if trying to select past last element, it |
| * returns the last element |
| */ |
| it.next = function() { |
| it.index = Math.min(it.items.length - 1, it.index + 1); |
| return $(it.items[it.index]); |
| }; |
| |
| /** Returns the previous entry - will return `undefined` instead if |
| * selecting up from first element |
| */ |
| it.prev = function() { |
| it.index = Math.max(-1, it.index - 1); |
| return it.index == -1 ? undefined : $(it.items[it.index]); |
| }; |
| |
| it.right = function() { |
| if (it.ritems.length != 0) { |
| it.items = it.ritems; |
| it.index = Math.min(it.index, it.items.length - 1); |
| } |
| return $(it.items[it.index]); |
| }; |
| |
| it.left = function() { |
| if (it.litems.length != 0) { |
| it.items = it.litems; |
| it.index = Math.min(it.index, it.items.length - 1); |
| } |
| return $(it.items[it.index]); |
| }; |
| }; |
| |
| function safeOffset($elem) { |
| return $elem.length ? $elem.offset() : { top:0, left:0 }; // offset relative to viewport |
| } |
| |
| /** Scroll helper, ensures that the selected elem is inside the viewport */ |
| var Scroller = function ($container) { |
| scroller = this; |
| scroller.container = $container; |
| |
| scroller.scrollDown = function($elem) { |
| var offset = safeOffset($elem); |
| if (offset !== undefined) { |
| var yPos = offset.top; |
| if ($container.height() < yPos || (yPos - $("#search").height()) < 0) { |
| $container.animate({ |
| scrollTop: $container.scrollTop() + yPos - $("#search").height() - 10 |
| }, 200); |
| } |
| } |
| }; |
| |
| scroller.scrollUp = function ($elem) { |
| var offset = safeOffset($elem); |
| if (offset !== undefined) { |
| var yPos = offset.top; |
| if (yPos < $("#search").height()) { |
| $container.animate({ |
| scrollTop: $container.scrollTop() + yPos - $("#search").height() - 10 |
| }, 200); |
| } |
| } |
| }; |
| |
| scroller.scrollTop = function() { |
| $container.animate({ |
| scrollTop: 0 |
| }, 200); |
| } |
| }; |
| |
| scheduler.add("init", function() { |
| $("#textfilter input").trigger("blur"); |
| var items = new EntityIterator( |
| $("div#results-content > div#entity-results > ul.entities span.entity > a").toArray(), |
| $("div#results-content > div#member-results > ul.entities span.entity > a").toArray() |
| ); |
| |
| var scroller = new Scroller($("#search-results")); |
| |
| var $old = items.next(); |
| $old.addClass("selected"); |
| scroller.scrollDown($old); |
| |
| $(window).on("keydown", function(e) { |
| switch ( e.keyCode ) { |
| case 9: // tab |
| $old.removeClass("selected"); |
| break; |
| |
| case 13: // enter |
| var href = $old.attr("href"); |
| location.replace(href); |
| $old.trigger("click"); |
| $("#textfilter input").val(""); |
| break; |
| |
| case 27: // escape |
| $("#textfilter input").val(""); |
| $("div#search-results").hide(); |
| $("#search > span.close-results").hide(); |
| $("#search > span#doc-title").show(); |
| break; |
| |
| case 37: // left |
| var oldTop = safeOffset($old).top; |
| $old.removeClass("selected"); |
| $old = items.left(); |
| $old.addClass("selected"); |
| |
| (oldTop - safeOffset($old).top < 0 ? scroller.scrollDown : scroller.scrollUp)($old); |
| break; |
| |
| case 38: // up |
| $old.removeClass('selected'); |
| $old = items.prev(); |
| |
| if ($old === undefined) { // scroll past top |
| $(window).off("keydown"); |
| $("#textfilter input").trigger("focus"); |
| scroller.scrollTop(); |
| return false; |
| } else { |
| $old.addClass("selected"); |
| scroller.scrollUp($old); |
| } |
| break; |
| |
| case 39: // right |
| var oldTop = safeOffset($old).top; |
| $old.removeClass("selected"); |
| $old = items.right(); |
| $old.addClass("selected"); |
| |
| (oldTop - safeOffset($old).top < 0 ? scroller.scrollDown : scroller.scrollUp)($old); |
| break; |
| |
| case 40: // down |
| $old.removeClass("selected"); |
| $old = items.next(); |
| $old.addClass("selected"); |
| scroller.scrollDown($old); |
| break; |
| } |
| }); |
| }); |
| } |
| |
| /* Configures the text filter */ |
| function configureTextFilter() { |
| scheduler.add("init", function() { |
| var input = $("#textfilter input"); |
| input.on('keyup', function(event) { |
| switch ( event.keyCode ) { |
| case 27: // escape |
| input.val(""); |
| $("div#search-results").hide(); |
| $("#search > span.close-results").hide(); |
| $("#search > span#doc-title").show(); |
| break; |
| |
| case 38: // up arrow |
| return false; |
| |
| case 40: // down arrow |
| $(window).off("keydown"); |
| handleKeyNavigation(); |
| return false; |
| } |
| |
| searchAll(); |
| }); |
| }); |
| scheduler.add("init", function() { |
| $("#textfilter > .input > .clear").on("click", function() { |
| $("#textfilter input").val(""); |
| $("div#search-results").hide(); |
| $("#search > span.close-results").hide(); |
| $("#search > span#doc-title").show(); |
| |
| $(this).hide(); |
| }); |
| }); |
| |
| scheduler.add("init", function() { |
| $("div#search > span.close-results").on("click", function() { |
| $("div#search-results").hide(); |
| $("#search > span.close-results").hide(); |
| $("#search > span#doc-title").show(); |
| $("#textfilter input").val(""); |
| }); |
| }); |
| } |
| |
| function compilePattern(query) { |
| var escaped = query.replace(/([\.\*\+\?\|\(\)\[\]\\])/g, '\\$1'); |
| |
| if (query.toLowerCase() != query) { |
| // Regexp that matches CamelCase subbits: "BiSe" is |
| // "[a-z]*Bi[a-z]*Se" and matches "BitSet", "ABitSet", ... |
| return new RegExp(escaped.replace(/([A-Z])/g,"[a-z]*$1")); |
| } |
| else { // if query is all lower case make a normal case insensitive search |
| return new RegExp(escaped, "i"); |
| } |
| } |
| |
| /** Searches packages for entites matching the search query using a regex |
| * |
| * @param {[Object]} pack: package being searched |
| * @param {RegExp} regExp: a regular expression for finding matching entities |
| */ |
| function searchPackage(pack, regExp) { |
| scheduler.add("search", function() { |
| var entities = Index.PACKAGES[pack]; |
| var matched = []; |
| var notMatching = []; |
| |
| scheduler.add("search", function() { |
| searchMembers(entities, regExp, pack); |
| }); |
| |
| entities.forEach(function (elem) { |
| regExp.test(elem.name) ? matched.push(elem) : notMatching.push(elem); |
| }); |
| |
| var results = { |
| "matched": matched, |
| "package": pack |
| }; |
| |
| scheduler.add("search", function() { |
| handleSearchedPackage(results, regExp); |
| setProgress(); |
| }); |
| }); |
| } |
| |
| function searchMembers(entities, regExp, pack) { |
| var memDiv = document.getElementById("member-results"); |
| var packLink = document.createElement("a"); |
| packLink.className = "package"; |
| packLink.appendChild(document.createTextNode(pack)); |
| packLink.style.display = "none"; |
| packLink.title = pack; |
| packLink.href = toRoot + urlFriendlyEntity(pack).replace(new RegExp("\\.", "g"), "/") + "/index.html"; |
| memDiv.appendChild(packLink); |
| |
| var entityUl = document.createElement("ul"); |
| entityUl.className = "entities"; |
| memDiv.appendChild(entityUl); |
| |
| entities.forEach(function(entity) { |
| var entityLi = document.createElement("li"); |
| var name = entity.name.split('.').pop() |
| |
| var iconElem = document.createElement("a"); |
| iconElem.className = "icon " + entity.kind; |
| iconElem.title = name + " " + entity.kind; |
| iconElem.href = toRoot + entity[entity.kind]; |
| entityLi.appendChild(iconElem); |
| |
| if (entity.kind != "object" && entity.object) { |
| var companion = document.createElement("a"); |
| companion.className = "icon object"; |
| companion.title = name + " companion object"; |
| companion.href = toRoot + entity.object; |
| entityLi.insertBefore(companion, iconElem); |
| } else { |
| var spacer = document.createElement("div"); |
| spacer.className = "icon spacer"; |
| entityLi.insertBefore(spacer, iconElem); |
| } |
| |
| var nameElem = document.createElement("span"); |
| nameElem.className = "entity"; |
| |
| var entityUrl = document.createElement("a"); |
| entityUrl.title = entity.shortDescription ? entity.shortDescription : name; |
| entityUrl.href = toRoot + entity[entity.kind]; |
| entityUrl.appendChild(document.createTextNode(name)); |
| |
| nameElem.appendChild(entityUrl); |
| entityLi.appendChild(nameElem); |
| |
| var membersUl = document.createElement("ul"); |
| membersUl.className = "members"; |
| entityLi.appendChild(membersUl); |
| |
| |
| searchEntity(entity, membersUl, regExp) |
| .then(function(res) { |
| if (res.length > 0) { |
| packLink.style.display = "block"; |
| entityUl.appendChild(entityLi); |
| } |
| }); |
| }); |
| } |
| |
| /** This function inserts `li` into the `ul` ordered by the li's id |
| * |
| * @param {Node} ul: the list in which to insert `li` |
| * @param {Node} li: item to insert |
| */ |
| function insertSorted(ul, li) { |
| var lis = ul.childNodes; |
| var beforeLi = null; |
| |
| for (var i = 0; i < lis.length; i++) { |
| if (lis[i].id > li.id) |
| beforeLi = lis[i]; |
| } |
| |
| // if beforeLi == null, it will be inserted last |
| ul.insertBefore(li, beforeLi); |
| } |
| |
| /** Defines the callback when a package has been searched and searches its |
| * members |
| * |
| * It will search all entities which matched the regExp. |
| * |
| * @param {Object} res: this is the searched package. It will contain the map |
| * from the `searchPackage`function. |
| * @param {RegExp} regExp |
| */ |
| function handleSearchedPackage(res, regExp) { |
| $("div#search-results").show(); |
| $("#search > span.close-results").show(); |
| $("#search > span#doc-title").hide(); |
| |
| var searchRes = document.getElementById("results-content"); |
| var entityDiv = document.getElementById("entity-results"); |
| |
| var packLink = document.createElement("a"); |
| packLink.className = "package"; |
| packLink.title = res.package; |
| packLink.href = toRoot + urlFriendlyEntity(res.package).replace(new RegExp("\\.", "g"), "/") + "/index.html"; |
| packLink.appendChild(document.createTextNode(res.package)); |
| |
| if (res.matched.length == 0) |
| packLink.style.display = "none"; |
| |
| entityDiv.appendChild(packLink); |
| |
| var ul = document.createElement("ul") |
| ul.className = "entities"; |
| |
| // Generate html list items from results |
| res.matched |
| .map(function(entity) { return listItem(entity, regExp); }) |
| .forEach(function(li) { ul.appendChild(li); }); |
| |
| entityDiv.appendChild(ul); |
| } |
| |
| /** Searches an entity asynchronously for regExp matches in an entity's members |
| * |
| * @param {Object} entity: the entity to be searched |
| * @param {Node} ul: the list in which to insert the list item created |
| * @param {RegExp} regExp |
| */ |
| function searchEntity(entity, ul, regExp) { |
| return new Promise(function(resolve, reject) { |
| var allMembers = |
| (entity.members_trait || []) |
| .concat(entity.members_class || []) |
| .concat(entity.members_object || []) |
| |
| var matchingMembers = $.grep(allMembers, function(member, i) { |
| return regExp.test(member.label); |
| }); |
| |
| resolve(matchingMembers); |
| }) |
| .then(function(res) { |
| res.forEach(function(elem) { |
| var kind = document.createElement("span"); |
| kind.className = "kind"; |
| kind.appendChild(document.createTextNode(elem.kind)); |
| |
| var label = document.createElement("a"); |
| label.title = elem.label; |
| label.href = toRoot + elem.link; |
| label.className = "label"; |
| label.appendChild(document.createTextNode(elem.label)); |
| |
| var tail = document.createElement("span"); |
| tail.className = "tail"; |
| tail.appendChild(document.createTextNode(elem.tail)); |
| |
| var li = document.createElement("li"); |
| li.appendChild(kind); |
| li.appendChild(label); |
| li.appendChild(tail); |
| |
| ul.appendChild(li); |
| }); |
| return res; |
| }); |
| } |
| |
| /** Creates a list item representing an entity |
| * |
| * @param {Object} entity, the searched entity to be displayed |
| * @param {RegExp} regExp |
| * @return {Node} list item containing entity |
| */ |
| function listItem(entity, regExp) { |
| var name = entity.name.split('.').pop() |
| var nameElem = document.createElement("span"); |
| nameElem.className = "entity"; |
| |
| var entityUrl = document.createElement("a"); |
| entityUrl.title = entity.shortDescription ? entity.shortDescription : name; |
| entityUrl.href = toRoot + entity[entity.kind]; |
| |
| entityUrl.appendChild(document.createTextNode(name)); |
| nameElem.appendChild(entityUrl); |
| |
| var iconElem = document.createElement("a"); |
| iconElem.className = "icon " + entity.kind; |
| iconElem.title = name + " " + entity.kind; |
| iconElem.href = toRoot + entity[entity.kind]; |
| |
| var li = document.createElement("li"); |
| li.id = entity.name.replace(new RegExp("\\.", "g"),"-"); |
| li.appendChild(iconElem); |
| li.appendChild(nameElem); |
| |
| if (entity.kind != "object" && entity.object) { |
| var companion = document.createElement("a"); |
| companion.title = name + " companion object"; |
| companion.href = toRoot + entity.object; |
| companion.className = "icon object"; |
| li.insertBefore(companion, iconElem); |
| } else { |
| var spacer = document.createElement("div"); |
| spacer.className = "icon spacer"; |
| li.insertBefore(spacer, iconElem); |
| } |
| |
| var ul = document.createElement("ul"); |
| ul.className = "members"; |
| |
| li.appendChild(ul); |
| |
| return li; |
| } |
| |
| /** Searches all packages and entities for the current search string in |
| * the input field "#textfilter" |
| * |
| * Then shows the results in div#search-results |
| */ |
| function searchAll() { |
| scheduler.clear("search"); // clear previous search |
| maxJobs = 1; // clear previous max |
| var searchStr = ($("#textfilter input").val() || '').trim(); |
| |
| if (searchStr === '') { |
| $("div#search-results").hide(); |
| $("#search > span.close-results").hide(); |
| $("#search > span#doc-title").show(); |
| return; |
| } |
| |
| // Replace ?search=X with current search string if not hosted locally on Chrome |
| try { |
| window.history.replaceState({}, "", "?search=" + searchStr); |
| } catch(e) {} |
| |
| $("div#results-content > span.search-text").remove(); |
| |
| var memberResults = document.getElementById("member-results"); |
| memberResults.innerHTML = ""; |
| var memberH1 = document.createElement("h1"); |
| memberH1.className = "result-type"; |
| memberH1.innerHTML = "Member results"; |
| memberResults.appendChild(memberH1); |
| |
| var entityResults = document.getElementById("entity-results"); |
| entityResults.innerHTML = ""; |
| var entityH1 = document.createElement("h1"); |
| entityH1.className = "result-type"; |
| entityH1.innerHTML = "Entity results"; |
| entityResults.appendChild(entityH1); |
| |
| $("div#results-content").prepend( |
| $("<span>") |
| .addClass("search-text") |
| .append(document.createTextNode(" Showing results for ")) |
| .append($("<span>").addClass("query-str").text(searchStr)) |
| ); |
| |
| var regExp = compilePattern(searchStr); |
| |
| // Search for all entities matching query |
| Index |
| .keys(Index.PACKAGES) |
| .sort() |
| .forEach(function(elem) { searchPackage(elem, regExp); }) |
| } |
| |
| /** Check if user agent is associated with a known mobile browser */ |
| function isMobile() { |
| return /Android|webOS|Mobi|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); |
| } |
| |
| function urlFriendlyEntity(entity) { |
| var corr = { |
| '\\+': '$plus', |
| ':': '$colon' |
| }; |
| |
| for (k in corr) |
| entity = entity.replace(new RegExp(k, 'g'), corr[k]); |
| |
| return entity; |
| } |
| |
| var maxJobs = 1; |
| function setProgress() { |
| var running = scheduler.numberOfJobs("search"); |
| maxJobs = Math.max(maxJobs, running); |
| |
| var percent = 100 - (running / maxJobs * 100); |
| var bar = document.getElementById("progress-fill"); |
| bar.style.height = "100%"; |
| bar.style.width = percent + "%"; |
| |
| if (percent == 100) { |
| setTimeout(function() { |
| bar.style.height = 0; |
| }, 500); |
| } |
| } |