// © 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").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]); }; }; /** 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 yPos = $elem.offset().top; // offset relative to viewport if ($container.height() < yPos || (yPos - $("#search").height()) < 0) { $container.animate({ scrollTop: $container.scrollTop() + yPos - $("#search").height() - 10 }, 200); } }; scroller.scrollUp = function ($elem) { var yPos = $elem.offset().top; // offset relative to viewport 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").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).bind("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.click(); $("#textfilter input").attr("value", ""); break; case 27: // escape $("#textfilter input").attr("value", ""); $("div#search-results").hide(); $("#search > span.close-results").hide(); $("#search > span#doc-title").show(); break; case 37: // left var oldTop = $old.offset().top; $old.removeClass("selected"); $old = items.left(); $old.addClass("selected"); (oldTop - $old.offset().top < 0 ? scroller.scrollDown : scroller.scrollUp)($old); break; case 38: // up $old.removeClass('selected'); $old = items.prev(); if ($old === undefined) { // scroll past top $(window).unbind("keydown"); $("#textfilter input").focus(); scroller.scrollTop(); return false; } else { $old.addClass("selected"); scroller.scrollUp($old); } break; case 39: // right var oldTop = $old.offset().top; $old.removeClass("selected"); $old = items.right(); $old.addClass("selected"); (oldTop - $old.offset().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.bind('keyup', function(event) { switch ( event.keyCode ) { case 27: // escape input.attr("value", ""); $("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).unbind("keydown"); handleKeyNavigation(); return false; } searchAll(); }); }); scheduler.add("init", function() { $("#textfilter > .input > .clear").click(function() { $("#textfilter input").attr("value", ""); $("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").click(function() { $("div#search-results").hide(); $("#search > span.close-results").hide(); $("#search > span#doc-title").show(); $("#textfilter input").attr("value", ""); }); }); } 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").attr("value").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("" +" Showing results for \"" + 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); } }