From ffcb50f8c2b06544adf7d601c9fdad391eafbdfb Mon Sep 17 00:00:00 2001 From: Alexandre Terrasa Date: Tue, 17 Apr 2012 12:17:37 -0400 Subject: [PATCH] nitdoc: Add quick search results preview. Signed-off-by: Alexandre Terrasa --- share/nitdoc/scripts/js-facilities.js | 212 ++++++++++++++++++++++++++------- share/nitdoc/styles/main.css | 35 +++++- src/nitdoc.nit | 102 +++++++++++++++- tests/sav/nitdoc_args1.sav | 1 + 4 files changed, 305 insertions(+), 45 deletions(-) diff --git a/share/nitdoc/scripts/js-facilities.js b/share/nitdoc/scripts/js-facilities.js index 669ad08..d67ef9c 100644 --- a/share/nitdoc/scripts/js-facilities.js +++ b/share/nitdoc/scripts/js-facilities.js @@ -5,6 +5,16 @@ $.expr[':'].icontains = function(obj, index, meta, stack){ return (obj.textContent.replace(/\[[0-9]+\]/g, "") || obj.innerText.replace(/\[[0-9]+\]/g, "") || jQuery(obj).text().replace(/\[[0-9]+\]/g, "") || '').toLowerCase().indexOf(meta[3].toLowerCase()) >= 0; }; +/* + * Quick Search global vars + */ + +// Current search results preview table +var currentTable = null; + +//Hightlighted index in search result preview table +var currentIndex = -1; + /* * Add folding and filtering facilities to class description page. @@ -37,48 +47,166 @@ $(document).ready(function() { $(this).nextAll().toggle(); }) - // Instert search field - $("nav.main ul") - .append( - $(document.createElement("li")) - .append( - $(document.createElement("form")) - .append( - $(document.createElement("input")) - .attr({ - id: "search", - type: "text", - value: "quick search..." - }) - .addClass("notUsed") - .keyup(function() { - $(this).parent().parent().find("ul li:not(:icontains('" + $(this).val() + "'))").addClass("hide"); - $(this).parent().parent().find("ul li:icontains('" + $(this).val() + "')").removeClass("hide"); - }) - .focusout(function() { - if($(this).val() == "") { - $(this).addClass("notUsed"); - $(this).val("quick search..."); - } - }) - .focusin(function() { - if($(this).val() == "quick search...") { - $(this).removeClass("notUsed"); - $(this).val(""); - } - }) - ) - .submit( function() { - if($("#search").val().length == 0) - return false - - window.location = "full-index.html#q=" + $("#search").val(); - if(window.location.href.indexOf("full-index.html") > -1) { - location.reload(); - } - return false; - }) - ) + // Insert search field + $("nav.main ul") + .append( + $(document.createElement("li")) + .append( + $(document.createElement("form")) + .append( + $(document.createElement("input")) + .attr({ + id: "search", + type: "text", + autocomplete: "off", + value: "quick search..." + }) + .addClass("notUsed") + + // Key management + .keyup(function(e) { + switch(e.keyCode) { + + // Select previous result on "Up" + case 38: + // If already on first result, focus search input + if(currentIndex == 0) { + $("#search").val($(currentTable.find("tr")[currentIndex]).data("searchDetails").name); + $("#search").focus(); + // Else select previous result + } else if(currentIndex > 0) { + $(currentTable.find("tr")[currentIndex]).removeClass("activeSearchResult"); + currentIndex--; + $(currentTable.find("tr")[currentIndex]).addClass("activeSearchResult"); + $("#search").val($(currentTable.find("tr")[currentIndex]).data("searchDetails").name); + $("#search").focus(); + } + break; + + // Select next result on "Down" + case 40: + if(currentIndex < currentTable.find("tr").length - 1) { + $(currentTable.find("tr")[currentIndex]).removeClass("activeSearchResult"); + currentIndex++; + $(currentTable.find("tr")[currentIndex]).addClass("activeSearchResult"); + $("#search").val($(currentTable.find("tr")[currentIndex]).data("searchDetails").name); + $("#search").focus(); + } + break; + // Go to url on "Enter" + case 13: + if(currentIndex > -1) { + window.location = $(currentTable.find("tr")[currentIndex]).data("searchDetails").url; + return false; + } + if($("#search").val().length == 0) + return false + + window.location = "full-index.html#q=" + $("#search").val(); + if(window.location.href.indexOf("full-index.html") > -1) { + location.reload(); + } + return false; + break; + + // Hide results preview on "Escape" + case 27: + $(this).blur(); + if(currentTable != null) { + currentTable.remove(); + currentTable = null; + } + break; + + default: + if($("#search").val().length == 0) { + return false; + } + + // Remove previous table + if(currentTable != null) { + currentTable.remove(); + } + + // Build results table + currentIndex = -1; + currentTable = $(document.createElement("table")); + + // Escape regexp related characters in query + var query = $("#search").val(); + query = query.replace(/\[/gi, "\\["); + query = query.replace(/\|/gi, "\\|"); + query = query.replace(/\*/gi, "\\*"); + query = query.replace(/\+/gi, "\\+"); + query = query.replace(/\\/gi, "\\\\"); + query = query.replace(/\?/gi, "\\?"); + query = query.replace(/\(/gi, "\\("); + query = query.replace(/\)/gi, "\\)"); + + var index = 0; + var regexp = new RegExp("^" + query, "i"); + for(var entry in entries) { + if(index > 10) { + break; + } + var result = entry.match(regexp); + if(result != null && result.toString().toUpperCase() == $("#search").val().toUpperCase()) { + for(var i = 0; i < entries[entry].length; i++) { + if(index > 10) { + break; + } + currentTable.append( + $(document.createElement("tr")) + .data("searchDetails", {name: entry, url: entries[entry][i]["url"]}) + .data("index", index) + .append($(document.createElement("td")).html(entry)) + .append( + $(document.createElement("td")) + .addClass("entryInfo") + .html(entries[entry][i]["txt"] + " »")) + .mouseover( function() { + $(currentTable.find("tr")[currentIndex]).removeClass("activeSearchResult"); + $(this).addClass("activeSearchResult"); + currentIndex = $(this).data("index"); + }) + .mouseout( function() { + $(this).removeClass("activeSearchResult"); + }) + .click( function() { + window.location = $(this).data("searchDetails")["url"]; + }) + ); + index++; + } + } + } + + // Initialize table properties + currentTable.attr("id", "searchTable"); + currentTable.css("position", "absolute"); + currentTable.width($("#search").outerWidth()); + $("header").append(currentTable); + currentTable.offset({left: $("#search").offset().left + ($("#search").outerWidth() - currentTable.outerWidth()), top: $("#search").offset().top + $("#search").outerHeight()}); + break; + } + }) + .focusout(function() { + if($(this).val() == "") { + $(this).addClass("notUsed"); + $(this).val("quick search..."); + } + }) + .focusin(function() { + if($(this).val() == "quick search...") { + $(this).removeClass("notUsed"); + $(this).val(""); + } + }) + ) + .submit( function() { + return false; + }) + ) ); /* diff --git a/share/nitdoc/styles/main.css b/share/nitdoc/styles/main.css index 253634e..07214ec 100644 --- a/share/nitdoc/styles/main.css +++ b/share/nitdoc/styles/main.css @@ -256,7 +256,6 @@ article .info .code { input[type=text] { width: 150px; border: 1px solid #CCC; - border-radius: 5px; margin-right: 5px; padding: 1px 2px; } @@ -321,4 +320,36 @@ nav h3 a.fold { .init.private { background-image: url('../resources/icons/const_private.png')} .fun.public { background-image: url('../resources/icons/meth_public.png')} .fun.protected { background-image: url('../resources/icons/meth_protected.png')} -.fun.private { background-image: url('../resources/icons/meth_private.png')} \ No newline at end of file +.fun.private { background-image: url('../resources/icons/meth_private.png')} + +/* Quick Search */ + +#search { + width: 300px; +} + +#searchTable { + background-color: #FFFFFF; + border: 1px solid #E0E0E0; + border-spacing: 0px; +} + +#searchTable .activeSearchResult { + cursor: pointer; + background: #EEE; +} + +#searchTable td { + white-space: nowrap; + overflow: hidden; + line-height: 22px; + padding: 2px; + width: 25%; +} + +#searchTable td.entryInfo { + color: #0D8921; + font-size: small; + width: 75%; + text-align: right; +} \ No newline at end of file diff --git a/src/nitdoc.nit b/src/nitdoc.nit index 2765264..03adf6b 100644 --- a/src/nitdoc.nit +++ b/src/nitdoc.nit @@ -157,7 +157,8 @@ class DocContext end var head = "" + - "\n" + + "\n" + + "\n" + "\n" + "" @@ -259,6 +260,11 @@ class DocContext add("
{footer_text}
") add("\n") write_to("{dir}/full-index.html") + + self.filename = "quicksearch-list" + clear + mainmod.file_quicksearch_list_doc(self) + write_to("{dir}/quicksearch-list.js") end @@ -437,6 +443,13 @@ class MMEntity # The doc node from the AST # Return null is none fun doc: nullable ADoc do return null + + # Return a jason entry for quicksearch list JSON Object + fun json_entry(dctx: DocContext): String is abstract + + # Return the qualified name as string + fun qualified_name: String is abstract + end redef class MMModule @@ -445,6 +458,19 @@ redef class MMModule return "{self}" end + redef fun json_entry(dctx) do + return "\{txt:\"{self.qualified_name}\",url:\"{html_name}.html\"\}," + end + + redef fun qualified_name do + var buffer = new Buffer + for m in mnhe.smallers do + buffer.append("{m.html_name}::") + end + buffer.append("{self.name}") + return buffer.to_s + end + fun require_doc(dctx: DocContext): Bool do if dctx.public_only and not is_toplevel then return false @@ -787,8 +813,66 @@ redef class MMModule dctx.stage("\n") dctx.close_stage end + + # Fill the quicksearch list JSON object + fun file_quicksearch_list_doc(dctx: DocContext) + do + var entities = new HashMap[String, Array[MMEntity]] + var props = new HashMap[MMGlobalProperty, Array[MMLocalProperty]] + for m in mhe.greaters_and_self do + if not m.require_doc(dctx) then continue + var a = new Array[MMEntity] + a.add(m) + entities[m.html_name] = a + end + for g in global_classes do + var lc = self[g] + if not lc.require_doc(dctx) then continue + var a = new Array[MMEntity] + a.add(lc) + entities[lc.html_name] = a + for gp in lc.global_properties do + var lp = lc[gp] + if not lp.require_doc(dctx) then continue + if props.has_key(lp.global) then + if not props[lp.global].has(lp) then + props[lp.global].add(lp) + end + else + props[lp.global] = [lp] + end + end + end + + for k, v in props do + entities[k.short_name] = v + end + + var keys = entities.keys.to_a + var sorter = new AlphaSorter[String] + sorter.sort(keys) + + dctx.open_stage + dctx.stage("var entries = \{") + for key in keys do + dctx.add("\"{key}\": [") + for entity in entities[key] do + dctx.add(entity.json_entry(dctx)) + end + dctx.add("],") + end + dctx.stage("\};") + dctx.close_stage + end end +redef class MMGlobalProperty + # Return the short name of the property + fun short_name: String do + return self.intro.html_name + end +end + redef class MMLocalProperty super MMEntity # Anchor of the property description in the module html file @@ -797,6 +881,14 @@ redef class MMLocalProperty return "PROP_{local_class}_{cmangle(name)}" end + redef fun json_entry(dctx) do + return "\{txt:\"{qualified_name}\",url:\"{local_class.html_name}.html#{html_anchor}\"\}," + end + + redef fun qualified_name do + return "{intro_module.qualified_name}::{local_class.html_name}::{html_name}" + end + fun html_open_link(dctx: DocContext): String do if not require_doc(dctx) then print "not required {self}" @@ -1107,6 +1199,14 @@ redef class MMLocalClass return "{self}" end + redef fun json_entry(dctx) do + return "\{txt:\"{qualified_name}\",url:\"{html_name}.html\"\}," + end + + redef fun qualified_name do + return "{intro_module.qualified_name}::{html_name}" + end + redef fun short_doc do return global.intro.short_doc redef fun doc do return global.intro.doc diff --git a/tests/sav/nitdoc_args1.sav b/tests/sav/nitdoc_args1.sav index 4a7eeb6..910352f 100644 --- a/tests/sav/nitdoc_args1.sav +++ b/tests/sav/nitdoc_args1.sav @@ -557,6 +557,7 @@ opts.html opts.map opts.png opts.s.dot +quicksearch-list.js range.dot range.html range.map -- 1.7.9.5