A semantic search engine for source code https://bitshift.benkurtovic.com/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

448 lines
13 KiB

  1. /*
  2. * @file Manages all library initialization, jQuery callbacks, query entry
  3. * callbacks, server querying, and results diplay for `index.html`.
  4. */
  5. var advancedSearchDiv = $("div#advanced-search");
  6. var advancedSearchButton = $("button#advanced-search");
  7. FINISH_TYPING_INTERVAL = 650;
  8. var searchBar = $("form#search-bar input[type='text']")[0];
  9. var resultsDiv = $("div#results")[0];
  10. var typingTimer, scrollTimer, lastValue;
  11. var searchResultsPage = 1;
  12. /*
  13. * Set all page callbacks.
  14. */
  15. (function setHomePageCallbabacks(){
  16. var results = $('#results').get(0);
  17. // Enable infinite scrolling down the results page.
  18. $(window).scroll(function(){
  19. if($(window).scrollTop() + $(window).height() == $(document).height() &&
  20. resultsDiv.querySelectorAll(".result").length > 0)
  21. loadMoreResults();
  22. clearTimeout(scrollTimer);
  23. if (!results.classList.contains('disable-hover'))
  24. results.classList.add('disable-hover')
  25. scrollTimer = setTimeout(function(){
  26. if (results.classList.contains('disable-hover'))
  27. results.classList.remove('disable-hover');
  28. }, 200);
  29. });
  30. // Toggle the advanced-search form's visibility.
  31. advancedSearchButton.click(function(){
  32. var searchField = $("div#search-field");
  33. if(!advancedSearchDiv.hasClass("visible")){
  34. searchField.addClass("partly-visible");
  35. advancedSearchDiv.fadeIn(500).addClass("visible");
  36. advancedSearchButton.addClass("clicked");
  37. }
  38. else {
  39. advancedSearchDiv.hide().removeClass("visible");
  40. advancedSearchButton.removeClass("clicked");
  41. if($("div#results .result").length == 0)
  42. searchField.removeClass("partly-visible");
  43. clearResults();
  44. }
  45. });
  46. // Enable capturing the `enter` key.
  47. $("form#search-bar").submit(function(event){
  48. event.preventDefault();
  49. return false;
  50. });
  51. searchBar.onkeyup = typingTimer;
  52. }());
  53. /*
  54. * Set keyboard shortcut mappings.
  55. */
  56. (function resultsHotkeys(){
  57. /*
  58. * If the currently viewed result is not the first, scroll to the previous
  59. * result.
  60. */
  61. var previousResult = function(){
  62. var currResult = $(".display-all");
  63. if(currResult.length) {
  64. currResult.removeClass("display-all");
  65. currResult = currResult.closest(".result").prev(".result");
  66. } else {
  67. currResult = $(document.querySelectorAll(".result")[0]);
  68. }
  69. currResult.addClass("display-all");
  70. currResult.each(function(){
  71. $('html,body').stop().animate({
  72. scrollTop: $(this).offset().top - (
  73. $(window).height() - $(this).outerHeight(true)) / 2
  74. }, 140);
  75. });
  76. };
  77. /*
  78. * If the currently viewed result is not the last, scroll to the next
  79. * result.
  80. */
  81. var nextResult = function(){
  82. var currResult = $(".display-all");
  83. if(currResult.length) {
  84. currResult.removeClass("display-all");
  85. currResult = currResult.closest(".result").next(".result");
  86. } else {
  87. currResult = $(document.querySelectorAll(".result")[0]);
  88. }
  89. currResult.addClass('display-all');
  90. currResult.each(function(){
  91. $('html,body').stop().animate({
  92. scrollTop: $(this).offset().top - (
  93. $(window).height() - $(this).outerHeight(true)) / 2
  94. }, 140);
  95. });
  96. };
  97. var displayHotkeyHelp = function(){
  98. var help = $("div#hotkey-help");
  99. if(help.hasClass("hidden"))
  100. help.fadeIn(420);
  101. else
  102. help.fadeOut(420);
  103. $("div#body").toggleClass("faded");
  104. help.toggleClass("hidden");
  105. }
  106. var hotkeyActions = {
  107. "k" : previousResult,
  108. "j" : nextResult,
  109. "h" : previousSymbolMatch,
  110. "l" : nextSymbolMatch,
  111. "?" : displayHotkeyHelp
  112. };
  113. $(window).keypress(function(key){
  114. for(var hotkey in hotkeyActions){
  115. var keyChar = String.fromCharCode(key.keyCode);
  116. if(keyChar == hotkey &&
  117. !($(key.target).is("textarea") || $(key.target).is("input")))
  118. hotkeyActions[keyChar]();
  119. }
  120. });
  121. }());
  122. // Enable infinite scrolling down the results page.
  123. $(window).scroll(function() {
  124. var searchField = $("div#search-field");
  125. if($(window).scrollTop() + $(window).height() == $(document).height() &&
  126. searchField.hasClass('partly-visible')){
  127. loadMoreResults();
  128. }
  129. });
  130. /*
  131. * Clear the existing timer and set a new one the the user types text into the
  132. * search bar.
  133. */
  134. function typingTimer(event){
  135. clearTimeout(typingTimer);
  136. var enterKeyCode = 13;
  137. if(event.keyCode != enterKeyCode){
  138. if(lastValue != searchBar.value)
  139. typingTimer = setTimeout(finishedTyping, FINISH_TYPING_INTERVAL);
  140. }
  141. else {
  142. event.preventDefault();
  143. finishedTyping();
  144. return false;
  145. }
  146. };
  147. /*
  148. * Callback which queries the server whenver the user stops typing.
  149. *
  150. * Whenever the user doesn't type for a `FINISH_TYPING_INTERVAL` after having
  151. * entered new text in the search bar, send the current query request to the
  152. * server.
  153. */
  154. function finishedTyping(){
  155. lastValue = searchBar.value;
  156. var searchField = $("div#search-field");
  157. clearResults();
  158. if(searchBar.value){
  159. searchField.addClass("partly-visible");
  160. populateResults();
  161. }
  162. else {
  163. searchField.removeClass("partly-visible");
  164. $("div#advanced-search").fadeOut(50);
  165. advancedSearchButton.removeClass("clicked");
  166. clearResults();
  167. }
  168. }
  169. /*
  170. * Removes any child elements of `div#results`.
  171. */
  172. function clearResults(){
  173. while(resultsDiv.firstChild)
  174. resultsDiv.removeChild(resultsDiv.firstChild);
  175. }
  176. /*
  177. * Create a result element based upon a codelet instance.
  178. *
  179. * @return {Element} The result element.
  180. */
  181. function createResult(codelet) {
  182. var maxAttributeLength = 20;
  183. //Level 1
  184. var newDiv = document.createElement("div"),
  185. table = document.createElement("table"),
  186. row = document.createElement("tr");
  187. //Level 2
  188. var displayInfo = document.createElement("div"),
  189. codeElt = document.createElement("td"),
  190. hiddenInfoContainer = document.createElement("td"),
  191. hiddenInfo = document.createElement("div"),
  192. cycle = document.createElement("div");
  193. //Level 3
  194. var title = document.createElement("span"),
  195. site = document.createElement("span"),
  196. nextMatch = document.createElement("a"),
  197. prevMatch = document.createElement("a"),
  198. dateModified = document.createElement("div"),
  199. language = document.createElement("div"),
  200. dateCreated = document.createElement("div"),
  201. authors = document.createElement("div");
  202. //Classes and ID's
  203. newDiv.classList.add('result');
  204. displayInfo.id = 'display-info';
  205. codeElt.id = 'code';
  206. hiddenInfo.id = 'hidden-info';
  207. cycle.id = 'cycle-matches'
  208. title.id = 'title';
  209. site.id = 'site';
  210. nextMatch.id = 'next-match';
  211. nextMatch.href = '#';
  212. prevMatch.id = 'prev-match';
  213. prevMatch.href = '#';
  214. dateModified.id = 'date-modified';
  215. language.id = 'language';
  216. dateCreated.id = 'date-created';
  217. authors.id = 'authors';
  218. //Add the bulk of the html
  219. title.innerHTML = ' &raquo; <a href="' + codelet.url + '">'
  220. + codelet.name + '</a>';
  221. site.innerHTML = '<a href="' + codelet.origin[1] + '">' +
  222. codelet.origin[0] +'</a>';
  223. nextMatch.innerHTML = 'next match';
  224. prevMatch.innerHTML = 'prev match';
  225. language.innerHTML = 'Language: <span>' + codelet.lang + '</span>';
  226. dateModified.innerHTML = 'Last modified: <span>' + codelet.modified +
  227. '</span>';
  228. // Needs to be changed from int to string on the server
  229. dateCreated.innerHTML = 'Created: <span>' +
  230. codelet.created.substring(0, maxAttributeLength) + '</span>';
  231. var authorsHtml = 'Authors: <span>';
  232. var currLength = 0;
  233. var authorsList = [];
  234. for(var auth = 0; auth < codelet.authors.length; auth++){
  235. currLength += codelet.authors[auth].length;
  236. if(maxAttributeLength < currLength){
  237. authorsList.push("...");
  238. break;
  239. }
  240. else
  241. authorsList.push('<a href=#>' + codelet.authors[auth] + '</a>');
  242. }
  243. authors.innerHTML = "Authors: <span>" + authorsList.join(", ") + "</span>";
  244. // Needs to be processed on the server
  245. codeElt.innerHTML = '<div id=tablecontainer>' + codelet.code + '</div>';
  246. //Event binding
  247. $(newDiv).on('mousemove', function(e) {
  248. var holdCondition = $('.disable-hover');
  249. if(holdCondition.length == 0) {
  250. $(this).siblings().removeClass('display-all');
  251. $(this).addClass('display-all');
  252. }
  253. });
  254. $(newDiv).on('mouseleave', function(e) {
  255. var holdCondition = $('.disable-hover');
  256. if(holdCondition.length == 0)
  257. $(this).removeClass('display-all');
  258. });
  259. $(nextMatch).click(function(e) {
  260. e.stopPropagation();
  261. e.preventDefault();
  262. nextSymbolMatch();
  263. });
  264. $(prevMatch).click(function(e) {
  265. e.stopPropagation();
  266. e.preventDefault();
  267. previousSymbolMatch();
  268. });
  269. //Finish and append elements to parent elements
  270. hiddenInfo.appendChild(dateCreated);
  271. hiddenInfo.appendChild(dateModified);
  272. hiddenInfo.appendChild(language);
  273. hiddenInfo.appendChild(authors);
  274. hiddenInfoContainer.appendChild(hiddenInfo);
  275. row.appendChild(codeElt);
  276. row.appendChild(hiddenInfoContainer);
  277. table.appendChild(row);
  278. displayInfo.appendChild(site);
  279. displayInfo.appendChild(title);
  280. cycle.appendChild(prevMatch);
  281. cycle.appendChild(nextMatch);
  282. newDiv.appendChild(displayInfo);
  283. newDiv.appendChild(table);
  284. return newDiv;
  285. }
  286. function previousSymbolMatch() {
  287. var currResult = $(".display-all"),
  288. currMatch = currResult.find(".hll.current"),
  289. matches = currResult.find(".hll"),
  290. scrollDiv = currResult.find("#tablecontainer");
  291. if (currMatch.length == 0)
  292. currMatch = matches[0];
  293. else
  294. currMatch.removeClass('current');
  295. var index = matches.index(currMatch.get(0)) - 1;
  296. index = index <= 0 ? matches.length - 1 : index;
  297. var newMatch = $(matches[index]);
  298. scrollDiv.scrollTop(scrollDiv.scrollTop()
  299. - scrollDiv.height() / 2
  300. + newMatch.position().top + newMatch.height() / 2);
  301. newMatch.effect("highlight", {color: '#FFF'}, 750)
  302. newMatch.addClass('current');
  303. };
  304. function nextSymbolMatch() {
  305. var currResult = $(".display-all"),
  306. currMatch = currResult.find(".hll.current"),
  307. matches = currResult.find(".hll"),
  308. scrollDiv = currResult.find("#tablecontainer");
  309. if (currMatch.length == 0)
  310. currMatch = $(matches[0]);
  311. else
  312. currMatch.removeClass("current");
  313. var index = matches.index(currMatch.get(0)) + 1;
  314. index = index >= matches.length ? 0 : index;
  315. var newMatch = $(matches[index]);
  316. scrollDiv.scrollTop(scrollDiv.scrollTop()
  317. - scrollDiv.height() / 2
  318. + newMatch.position().top + newMatch.height() / 2);
  319. newMatch.effect("highlight", {color: "#FFF"}, 750)
  320. newMatch.addClass("current");
  321. };
  322. /*
  323. * AJAX the current query string to the server, and return its response.
  324. *
  325. * @return {Array} The server's response in the form of `div.result` DOM
  326. * elements, to fill `div#results`.
  327. */
  328. function queryServer(){
  329. var queryUrl = document.URL + "search.json?" + $.param({
  330. "q" : searchBar.value,
  331. "p" : searchResultsPage++,
  332. "hl": 1
  333. });
  334. var results = $.Deferred();
  335. $.getJSON(queryUrl, function(result){
  336. var resultDivs = [];
  337. if("error" in result)
  338. insertErrorMessage(result["error"]);
  339. else if(result["results"].length == 0 && searchResultsPage == 1)
  340. insertErrorMessage("No search results.");
  341. else
  342. for(var codelet = 0; codelet < result["results"].length; codelet++)
  343. resultDivs.push(createResult(result["results"][codelet]));
  344. results.resolve(resultDivs);
  345. });
  346. return results;
  347. }
  348. /*
  349. * Query the server with the current search string, and populate `div#results`
  350. * with its response.
  351. */
  352. function populateResults(){
  353. searchResultsPage = 1;
  354. loadMoreResults();
  355. }
  356. /*
  357. * Query the server for the next results page, and add its codelets to
  358. * `div#results`.
  359. */
  360. function loadMoreResults(){
  361. queryServer().done(function(results){
  362. for(var result = 0; result < results.length; result++){
  363. var newDiv = results[result];
  364. resultsDiv.appendChild(newDiv);
  365. setTimeout(
  366. (function(divReference){
  367. return function(){
  368. divReference.classList.add("cascade");
  369. };
  370. }(newDiv)),
  371. result * 20);
  372. }
  373. });
  374. }
  375. /*
  376. * Displays a warning message in the UI.
  377. *
  378. * @param msg (str) The message string.
  379. */
  380. function insertErrorMessage(msg){
  381. var error = $(
  382. [
  383. "<div id='error'><span id='s1'>Error</span> ",
  384. "<span id='s2'>&raquo;</span> </div>"
  385. ].join(""));
  386. error.append(msg);
  387. resultsDiv.appendChild(error[0]);
  388. }