packages feed

hoogle-5.0: html/hoogle.js

// PERHAPS I SHOULD BE USING Bootstrap with:
// http://silviomoreto.github.io/bootstrap-select/


var embed = false; // are we running as an embedded search box
var instant = true; // should we search on key presses
var query = parseQuery(); // what is the current query string

var $hoogle; // $("#hoogle") after load


/////////////////////////////////////////////////////////////////////
// SEARCHING

function on_arrow_press(ev) {
    var offset = 0;
    if (ev.keyCode == Key.Up) {
        offset = -1;
    } else if (ev.keyCode == Key.Down) {
        offset = +1;
    } else if (ev.keyCode != Key.Return) {
        return;
    }

    // Figure out where we are
    var results = $("div#body .result");
    var activeResults = $("div#body .result.active");
    var activeRow = -1;
    if (activeResults.length == 1) {
        activeRow = results.index(activeResults[0]);
    }

    if (ev.keyCode == Key.Return) {
        if (activeRow >= 0)
            document.location.href = $("a", activeResults).attr("href");
    } else {
        var newRow = activeRow + offset;
        var $activeRow = $(results[activeRow]);
        if (newRow < 0) {
            $activeRow.removeClass("active");
            $hoogle.focus();
        } else if (newRow < results.length) {
            var $newRow = $(results[newRow]);
            if (activeRow >= 0)
                $activeRow.removeClass("active");
            $newRow.addClass("active");
            $hoogle.blur();
        }
    }
}

$(function() {
    $(document).keyup(on_arrow_press);
});

$(function(){
    $hoogle = $("#hoogle");
    var $form = $hoogle.parents("form:first");
    var $scope = $form.find("[name=scope]");
    embed = !$hoogle.hasClass("HOOGLE_REAL");
    if (!embed) $scope.chosen({"search_contains":true});

    var self = embed ? newEmbed() : newReal();

    var ajaxUrl = !embed ? "?" : $form.attr("action") + "?";
    var ajaxMode = embed ? 'embed' : 'body';

    var active = $hoogle.val() + " " + $scope.val(); // What is currently being searched for (may not yet be displayed)
    var past = cache(100); // Cache of previous searches
    var watch = watchdog(500, function(){self.showWaiting();}); // Timeout of the "Waiting..." callback

    function hit(){
        if (!instant) return;
        function getScope(){return $scope && $scope.val() !== "set:stackage" ? $scope.val() : "";}

        var nowHoogle = $hoogle.val();
        var nowScope = getScope();
        var now = nowHoogle + " " + nowScope;
        if (now == active) return;
        active = now;

        var title = now + (now == " " ? "" : " - ") + "Hoogle";
        query["hoogle"] = nowHoogle;
        query["scope"] = nowScope;
        if (!embed){
            if (window.history)
                window.history.replaceState(null, title, renderQuery(query));
            $("title").text(title);
        }

        var old = past.ask(now);
        if (old != undefined){self.showResult(old); return;}

        watch.stop();
        if (embed && now == ""){self.hide(); return;}
        watch.start();

        var data = {hoogle:nowHoogle, scope:nowScope, mode:ajaxMode};
        function complete(e)
        {
            watch.stop();
            var current = $hoogle.val() + " " + getScope() == now;
            if (e.status == 200)
            {
                past.add(now,e.responseText);
                if (current)
                    self.showResult(e.responseText);
            }
            else if (current)
                self.showError(e.status, e.responseText);
        }

        var args = {url:ajaxUrl, data:data, complete:complete, dataType:"html"}
        try {
            $.ajax(args);
        } catch (err) {
            try {
                if (!embed) throw err;
                $.ajaxCrossDomain(args);
            } catch (err) {
                // Probably a permissions error from cross domain scripting...
                watch.stop();
            }
        }
    };
    $hoogle.keyup(hit);
    $scope.change(hit);
})

function newReal()
{
    $hoogle.focus();
    $hoogle.select();
    var $body = $("#body");

    return {
        showWaiting: function(){$("h1").text("Still working...");},
        showError: function(status,text){$body.html("<h1><b>Error:</b> status " + status + "</h1><p>" + text + "</p>")},
        showResult: function(text){$body.html(text); newDocs();}
    }
}

function newEmbed()
{
    $hoogle.attr("autocomplete","off");
    // IE note: unless the div in the iframe contain any border it doesn't calculate the correct outerHeight()
    //          therefore we put 3 borders on the iframe, and leave one for the bottom div
    var $iframe = $("<iframe id='hoogle-output' scrolling='no' "+
                    "style='position:absolute;border:1px solid rgb(127,157,185);border-bottom:0px;display:none;' />");
    var $body;
    $iframe.load(function(){
        var $contents = $iframe.contents();
        $contents.find("head").html(
            "<style type='text/css'>" +
            "html {border: 0px;}" +
            "body {font-family: sans-serif; font-size: 13px; background-color: white; padding: 0px; margin: 0px;}" +
            "a, i {display: block; color: black; padding: 1px 3px; text-decoration: none; white-space: nowrap; overflow: hidden; cursor: default;}" +
            "a.sel {background-color: rgb(10,36,106); color: white;}" +
            "div {border-bottom:1px solid rgb(127,157,185);}" +
            "</style>");
        $body = $("<div>").appendTo($contents.find("body"));
    });
    $iframe.insertBefore($hoogle);

    var finishOnBlur = true; // Should a blur hide the box

    function show(x){
        if (x == undefined)
            $iframe.css("display","none");
        else {
            $body.html(x).find("a").attr("target","_parent")
                .mousedown(function(){finishOnBlur = false;})
                .mouseup(function(){finishOnBlur = true;})
                .mouseenter(function(){
                    $body.find(".sel").removeClass("sel");
                    $(this).addClass("sel");
                });

            var pos = $hoogle.position();
            // need to display before using $body.outerHeight() on Firefox
            $iframe.css("display","").css(
                {top:px(pos.top + $hoogle.outerHeight() + unpx($hoogle.css("margin-top")))
                ,left:px(pos.left + unpx($hoogle.css("margin-left")))
                ,width:px($hoogle.outerWidth() - 2 /* iframe border */)
                ,height:$body.outerHeight()
                });
        }
    }

    $hoogle.blur(function(){if (finishOnBlur) show();});

    $hoogle.keydown(function(event){
        switch(event.which)
        {
        case Key.Return:
            var sel = $body.find(".sel:first");
            if (sel.size() == 0) return;
            event.preventDefault();
            document.location.href = sel.attr("href");
            break;

        case Key.Escape:
            $body.find(".sel").removeClass("sel");
            show();
            break;

        case Key.Down: case Key.Up:
            var i = event.which == Key.Down ? 1 : -1;
            var all = $body.find("a");
            var sel = all.filter(".sel");
            var now = all.index(sel);
            if (now == -1)
                all.filter(i == 1 ? ":first" : ":last").addClass("sel");
            else {
                sel.removeClass("sel");
                // IE treats :eq(-1) as :eq(0), so filter specifically
                if (now+i >= 0) all.filter(":eq(" + (now+i) + ")").addClass("sel");
            }
            event.preventDefault();
            break;
        }
    });

    return {
        showWaiting: function(){show("<i>Still working...</i>");},
        showError: function(status,text){show("<i>Error: status " + status + "</i>");},
        showResult: function(text){show(text);},
        hide: function(){show();}
    }
}


/////////////////////////////////////////////////////////////////////
// SEARCH PLUGIN

var prefixUrl = document.location.protocol + "//" + document.location.hostname + document.location.pathname;

$(function(){
    if (embed) return;
    if (prefixUrl != "http://hoogle.haskell.org/")
    {
        $("link[rel=search]").attr("href", function(){
            return this.href + "?domain=" + escape(prefixUrl);
        });
    }
    if (window.external && ("AddSearchProvider" in window.external))
        $("#plugin").css("display","");
});

function searchPlugin()
{
    var url = $("link[rel=search]").attr("href");
    if (url.substring(0, prefixUrl.length) != prefixUrl)
        url = prefixUrl + url;
    window.external.AddSearchProvider(url);
}


/////////////////////////////////////////////////////////////////////
// DOCUMENTATION

$(function(){
    if (embed) return;
    $(window).resize(resizeDocs);
    newDocs();
});

function resizeDocs()
{
    $("#body .doc").each(function(){
        // If a segment is open, it should remain open forever
        var $this = $(this);
        var toosmall = ($.support.preWrap && $this.hasClass("newline")) ||
                       ($this.height() < $this.children().height());
        if (toosmall && !$this.hasClass("open"))
            $this.addClass("shut");
        else if (!toosmall && $this.hasClass("shut"))
            $this.removeClass("shut");
    });
}

function newDocs()
{
    resizeDocs();
    $("#body .doc").click(function(){
        var $this = $(this);
        if ($this.hasClass("open") || $this.hasClass("shut"))
            $this.toggleClass("open").toggleClass("shut");
    });
}


/////////////////////////////////////////////////////////////////////
// iOS TWEAKS

$(function(){
    if ($.support.inputSearch)
        $("#hoogle")[0].type = "search";

    var qphone = query["phone"];
    phone =
        qphone == "0" ? false :
        qphone == "1" ? true :
        $.support.phone;

    if (!phone) return;
    $("body").addClass("phone");
    $("head").append("<meta name='viewport' content='width=device-width' />");
});


/////////////////////////////////////////////////////////////////////
// LIBRARY BITS

function parseQuery() // :: IO (Dict String String)
{
    // From http://stackoverflow.com/questions/901115/get-querystring-values-with-jquery/3867610#3867610
    var params = {},
        e,
        a = /\+/g,  // Regex for replacing addition symbol with a space
        r = /([^&=]+)=?([^&]*)/g,
        d = function (s) { return decodeURIComponent(s.replace(a, " ")); },
        q = window.location.search.substring(1);

    while (e = r.exec(q))
        params[d(e[1])] = d(e[2]);

    return params;
}

function renderQuery(query) // Dict String String -> IO String
{
    var s = "";
    for (var i in query)
    {
        if (query[i] != "")
            s += (s == "" ? "?" : "&") + i + "=" + encodeURIComponent(query[i]);
    }
    return window.location.href.substring(0, window.location.href.length - window.location.search.length) + s;
}


// Supports white-space: pre-wrap;
$.support.preWrap = true;

$.support.iOS =
    (navigator.userAgent.indexOf("iPhone") != -1) ||
    (navigator.userAgent.indexOf("iPod") != -1) ||
    (navigator.userAgent.indexOf("iPad") != -1);

$.support.phone =
    (navigator.userAgent.indexOf("iPhone") != -1) ||
    (navigator.userAgent.indexOf("iPod") != -1) ||
    (navigator.userAgent.indexOf("Android") != -1);

// Supports <input type=search />
$.support.inputSearch = $.support.iOS;

var Key = {
    Up: 38,
    Down: 40,
    Return: 13,
    Escape: 27
};

function unpx(x){var r = 1 * x.replace("px",""); return isNaN(r) ? 0 : r;}
function px(x){return x + "px";}

function cache(maxElems)
{
    // FIXME: Currently does not evict things
    var contents = {}; // what we have in the cache, with # prepended
    // note that contents[toString] != undefined, since it's a default method
    // hence the leading #

    return {
        add: function(key,val)
        {
            contents["#" + key] = val;
        },
        
        ask: function(key)
        {
            return contents["#" + key];
        }
    };
}

function watchdog(time, fun)
{
    var id = undefined;
    function stop(){if (id == undefined) return; window.clearTimeout(id); id = undefined;}
    function start(){stop(); id = window.setTimeout(function(){id = undefined; fun();}, time);}
    return {start:start, stop:stop}
}

$.ajaxCrossDomain = function(args)
{
    if (!window.XDomainRequest) throw new Error("the XDomainRequest object is not supported in this browser");

    var xdr = new XDomainRequest();
    xdr.onload = function(){args.complete({status:200, responseText:xdr.responseText});};
    xdr.onerror = function(){args.complete({status:0, responseText:""});};

    var url = "";
    for (var i in args.data)
    {
        if (args.data[i] == undefined) continue;
        url += (url == "" ? "" : "&") + encodeURIComponent(i) + "=" + encodeURIComponent(args.data[i]);
    }
    xdr.open("get", args.url + url);
    xdr.send();
}