PageRenderTime 55ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 1ms

/html/hoogle.js

http://github.com/ndmitchell/hoogle
JavaScript | 417 lines | 324 code | 66 blank | 27 comment | 87 complexity | a803f48be96a58076209b42e5b449e37 MD5 | raw file
Possible License(s): BSD-3-Clause, GPL-2.0
  1. // PERHAPS I SHOULD BE USING Bootstrap with:
  2. // http://silviomoreto.github.io/bootstrap-select/
  3. var embed = false; // are we running as an embedded search box
  4. var instant = true; // should we search on key presses
  5. var query = parseQuery(); // what is the current query string
  6. var $hoogle; // $("#hoogle") after load
  7. /////////////////////////////////////////////////////////////////////
  8. // SEARCHING
  9. function on_arrow_press(ev) {
  10. var offset = 0;
  11. if (ev.keyCode == Key.Up) {
  12. offset = -1;
  13. } else if (ev.keyCode == Key.Down) {
  14. offset = +1;
  15. } else if (ev.keyCode != Key.Return) {
  16. return;
  17. }
  18. // Figure out where we are
  19. var results = $("div#body .result");
  20. var activeResults = $("div#body .result.active");
  21. var activeRow = -1;
  22. if (activeResults.length == 1) {
  23. activeRow = results.index(activeResults[0]);
  24. }
  25. if (ev.keyCode == Key.Return) {
  26. if (activeRow >= 0)
  27. document.location.href = $("a", activeResults).attr("href");
  28. } else {
  29. var newRow = activeRow + offset;
  30. var $activeRow = $(results[activeRow]);
  31. if (newRow < 0) {
  32. $activeRow.removeClass("active");
  33. $hoogle.focus();
  34. } else if (newRow < results.length) {
  35. var $newRow = $(results[newRow]);
  36. if (activeRow >= 0)
  37. $activeRow.removeClass("active");
  38. $newRow.addClass("active");
  39. $hoogle.blur();
  40. }
  41. }
  42. }
  43. $(function() {
  44. $(document).keyup(on_arrow_press);
  45. });
  46. $(function(){
  47. $hoogle = $("#hoogle");
  48. var $form = $hoogle.parents("form:first");
  49. var $scope = $form.find("[name=scope]");
  50. embed = !$hoogle.hasClass("HOOGLE_REAL");
  51. if (!embed) $scope.chosen({"search_contains":true});
  52. var self = embed ? newEmbed() : newReal();
  53. var ajaxUrl = !embed ? "?" : $form.attr("action") + "?";
  54. var ajaxMode = embed ? 'embed' : 'body';
  55. var active = $hoogle.val() + " " + $scope.val(); // What is currently being searched for (may not yet be displayed)
  56. var past = cache(100); // Cache of previous searches
  57. var watch = watchdog(500, function(){self.showWaiting();}); // Timeout of the "Waiting..." callback
  58. function hit(){
  59. if (!instant) return;
  60. function getScope(){
  61. var v = $scope ? $scope.val() : "";
  62. return v == null || v == "set:stackage" ? "" : v;
  63. }
  64. var nowHoogle = $hoogle.val();
  65. var nowScope = getScope();
  66. var now = nowHoogle + " " + nowScope;
  67. if (now == active) return;
  68. active = now;
  69. var title = now + (now == " " ? "" : " - ") + "Hoogle";
  70. query["hoogle"] = nowHoogle;
  71. query["scope"] = nowScope;
  72. if (!embed){
  73. if (window.history)
  74. window.history.replaceState(null, title, renderQuery(query));
  75. $("title").text(title);
  76. }
  77. var old = past.ask(now);
  78. if (old != undefined){self.showResult(old); return;}
  79. watch.stop();
  80. if (embed && now == ""){self.hide(); return;}
  81. watch.start();
  82. var data = {hoogle:nowHoogle, scope:nowScope, mode:ajaxMode};
  83. function complete(e)
  84. {
  85. watch.stop();
  86. var current = $hoogle.val() + " " + getScope() == now;
  87. if (e.status == 200)
  88. {
  89. past.add(now,e.responseText);
  90. if (current)
  91. self.showResult(e.responseText);
  92. }
  93. else if (current)
  94. self.showError(e.status, e.responseText);
  95. }
  96. var args = {url:ajaxUrl, data:data, complete:complete, dataType:"html"}
  97. try {
  98. $.ajax(args);
  99. } catch (err) {
  100. try {
  101. if (!embed) throw err;
  102. $.ajaxCrossDomain(args);
  103. } catch (err) {
  104. // Probably a permissions error from cross domain scripting...
  105. watch.stop();
  106. }
  107. }
  108. };
  109. $hoogle.keyup(hit);
  110. $scope.change(hit);
  111. })
  112. function newReal()
  113. {
  114. $hoogle.select();
  115. var $body = $("#body");
  116. return {
  117. showWaiting: function(){$("h1").text("Still working...");},
  118. showError: function(status,text){$body.html("<h1><b>Error:</b> status " + status + "</h1><p>" + text + "</p>")},
  119. showResult: function(text){$body.html(text); newDocs();}
  120. }
  121. }
  122. function newEmbed()
  123. {
  124. $hoogle.attr("autocomplete","off");
  125. // IE note: unless the div in the iframe contain any border it doesn't calculate the correct outerHeight()
  126. // therefore we put 3 borders on the iframe, and leave one for the bottom div
  127. var $iframe = $("<iframe id='hoogle-output' scrolling='no' "+
  128. "style='position:absolute;border:1px solid rgb(127,157,185);border-bottom:0px;display:none;' />");
  129. var $body;
  130. $iframe.load(function(){
  131. var $contents = $iframe.contents();
  132. $contents.find("head").html(
  133. "<style type='text/css'>" +
  134. "html {border: 0px;}" +
  135. "body {font-family: sans-serif; font-size: 13px; background-color: white; padding: 0px; margin: 0px;}" +
  136. "a, i {display: block; color: black; padding: 1px 3px; text-decoration: none; white-space: nowrap; overflow: hidden; cursor: default;}" +
  137. "a.sel {background-color: rgb(10,36,106); color: white;}" +
  138. "div {border-bottom:1px solid rgb(127,157,185);}" +
  139. "</style>");
  140. $body = $("<div>").appendTo($contents.find("body"));
  141. });
  142. $iframe.insertBefore($hoogle);
  143. var finishOnBlur = true; // Should a blur hide the box
  144. function show(x){
  145. if (x == undefined)
  146. $iframe.css("display","none");
  147. else {
  148. $body.html(x).find("a").attr("target","_parent")
  149. .mousedown(function(){finishOnBlur = false;})
  150. .mouseup(function(){finishOnBlur = true;})
  151. .mouseenter(function(){
  152. $body.find(".sel").removeClass("sel");
  153. $(this).addClass("sel");
  154. });
  155. var pos = $hoogle.position();
  156. // need to display before using $body.outerHeight() on Firefox
  157. $iframe.css("display","").css(
  158. {top:px(pos.top + $hoogle.outerHeight() + unpx($hoogle.css("margin-top")))
  159. ,left:px(pos.left + unpx($hoogle.css("margin-left")))
  160. ,width:px($hoogle.outerWidth() - 2 /* iframe border */)
  161. ,height:$body.outerHeight()
  162. });
  163. }
  164. }
  165. $hoogle.blur(function(){if (finishOnBlur) show();});
  166. $hoogle.keydown(function(event){
  167. switch(event.which)
  168. {
  169. case Key.Return:
  170. var sel = $body.find(".sel:first");
  171. if (sel.size() == 0) return;
  172. event.preventDefault();
  173. document.location.href = sel.attr("href");
  174. break;
  175. case Key.Escape:
  176. $body.find(".sel").removeClass("sel");
  177. show();
  178. break;
  179. case Key.Down: case Key.Up:
  180. var i = event.which == Key.Down ? 1 : -1;
  181. var all = $body.find("a");
  182. var sel = all.filter(".sel");
  183. var now = all.index(sel);
  184. if (now == -1)
  185. all.filter(i == 1 ? ":first" : ":last").addClass("sel");
  186. else {
  187. sel.removeClass("sel");
  188. // IE treats :eq(-1) as :eq(0), so filter specifically
  189. if (now+i >= 0) all.filter(":eq(" + (now+i) + ")").addClass("sel");
  190. }
  191. event.preventDefault();
  192. break;
  193. }
  194. });
  195. return {
  196. showWaiting: function(){show("<i>Still working...</i>");},
  197. showError: function(status,text){show("<i>Error: status " + status + "</i>");},
  198. showResult: function(text){show(text);},
  199. hide: function(){show();}
  200. }
  201. }
  202. /////////////////////////////////////////////////////////////////////
  203. // SEARCH PLUGIN
  204. var prefixUrl = document.location.protocol + "//" + document.location.hostname + document.location.pathname;
  205. $(function(){
  206. if (embed) return;
  207. if (prefixUrl != "http://hoogle.haskell.org/")
  208. {
  209. $("link[rel=search]").attr("href", function(){
  210. return this.href + "?domain=" + escape(prefixUrl);
  211. });
  212. }
  213. if (window.external && ("AddSearchProvider" in window.external))
  214. {
  215. $("#plugin").css("display","inline").on('click', function(){
  216. var url = $("link[rel=search]").attr("href");
  217. // If neither scheme(http(s)://) nor DSN prefix(//) is in URL then we
  218. // should add prefix.
  219. if (url.indexOf('://') === -1 && url.indexOf('//') !== 0)
  220. url = prefixUrl + url;
  221. window.external.AddSearchProvider(url);
  222. });
  223. }
  224. });
  225. /////////////////////////////////////////////////////////////////////
  226. // DOCUMENTATION
  227. $(function(){
  228. if (embed) return;
  229. $(window).resize(resizeDocs);
  230. newDocs();
  231. });
  232. function resizeDocs()
  233. {
  234. $("#body .doc").each(function(){
  235. // If a segment is open, it should remain open forever
  236. var $this = $(this);
  237. var toosmall = ($.support.preWrap && $this.hasClass("newline")) ||
  238. ($this.height() < $this.children().height());
  239. if (toosmall && !$this.hasClass("open"))
  240. $this.addClass("shut");
  241. else if (!toosmall && $this.hasClass("shut"))
  242. $this.removeClass("shut");
  243. });
  244. }
  245. function newDocs()
  246. {
  247. resizeDocs();
  248. $("#body .doc").click(function(){
  249. var $this = $(this);
  250. if ($this.hasClass("open") || $this.hasClass("shut"))
  251. $this.toggleClass("open").toggleClass("shut");
  252. });
  253. }
  254. /////////////////////////////////////////////////////////////////////
  255. // iOS TWEAKS
  256. $(function(){
  257. if ($.support.inputSearch)
  258. $("#hoogle")[0].type = "search";
  259. var qphone = query["phone"];
  260. phone =
  261. qphone == "0" ? false :
  262. qphone == "1" ? true :
  263. $.support.phone;
  264. if (!phone) return;
  265. $("body").addClass("phone");
  266. $("head").append("<meta name='viewport' content='width=device-width' />");
  267. });
  268. /////////////////////////////////////////////////////////////////////
  269. // LIBRARY BITS
  270. function parseQuery() // :: IO (Dict String String)
  271. {
  272. // From http://stackoverflow.com/questions/901115/get-querystring-values-with-jquery/3867610#3867610
  273. var params = {},
  274. e,
  275. a = /\+/g, // Regex for replacing addition symbol with a space
  276. r = /([^&=]+)=?([^&]*)/g,
  277. d = function (s) { return decodeURIComponent(s.replace(a, " ")); },
  278. q = window.location.search.substring(1);
  279. while (e = r.exec(q))
  280. params[d(e[1])] = d(e[2]);
  281. return params;
  282. }
  283. function renderQuery(query) // Dict String String -> IO String
  284. {
  285. var s = "";
  286. for (var i in query)
  287. {
  288. if (query[i] != "")
  289. s += (s == "" ? "?" : "&") + i + "=" + encodeURIComponent(query[i]);
  290. }
  291. return window.location.href.substring(0, window.location.href.length - window.location.search.length) + s;
  292. }
  293. // Supports white-space: pre-wrap;
  294. $.support.preWrap = true;
  295. $.support.iOS =
  296. (navigator.userAgent.indexOf("iPhone") != -1) ||
  297. (navigator.userAgent.indexOf("iPod") != -1) ||
  298. (navigator.userAgent.indexOf("iPad") != -1);
  299. $.support.phone =
  300. (navigator.userAgent.indexOf("iPhone") != -1) ||
  301. (navigator.userAgent.indexOf("iPod") != -1) ||
  302. (navigator.userAgent.indexOf("Android") != -1);
  303. // Supports <input type=search />
  304. $.support.inputSearch = $.support.iOS;
  305. var Key = {
  306. Up: 38,
  307. Down: 40,
  308. Return: 13,
  309. Escape: 27
  310. };
  311. function unpx(x){var r = 1 * x.replace("px",""); return isNaN(r) ? 0 : r;}
  312. function px(x){return x + "px";}
  313. function cache(maxElems)
  314. {
  315. // FIXME: Currently does not evict things
  316. var contents = {}; // what we have in the cache, with # prepended
  317. // note that contents[toString] != undefined, since it's a default method
  318. // hence the leading #
  319. return {
  320. add: function(key,val)
  321. {
  322. contents["#" + key] = val;
  323. },
  324. ask: function(key)
  325. {
  326. return contents["#" + key];
  327. }
  328. };
  329. }
  330. function watchdog(time, fun)
  331. {
  332. var id = undefined;
  333. function stop(){if (id == undefined) return; window.clearTimeout(id); id = undefined;}
  334. function start(){stop(); id = window.setTimeout(function(){id = undefined; fun();}, time);}
  335. return {start:start, stop:stop}
  336. }
  337. $.ajaxCrossDomain = function(args)
  338. {
  339. if (!window.XDomainRequest) throw new Error("the XDomainRequest object is not supported in this browser");
  340. var xdr = new XDomainRequest();
  341. xdr.onload = function(){args.complete({status:200, responseText:xdr.responseText});};
  342. xdr.onerror = function(){args.complete({status:0, responseText:""});};
  343. var url = "";
  344. for (var i in args.data)
  345. {
  346. if (args.data[i] == undefined) continue;
  347. url += (url == "" ? "" : "&") + encodeURIComponent(i) + "=" + encodeURIComponent(args.data[i]);
  348. }
  349. xdr.open("get", args.url + url);
  350. xdr.send();
  351. }