don’t attach multiple instances of event handlers
[debiancodesearch.git] / static / instant.js
blob09ac733a256bea219341c1b714809d8c879029d0
1 // © 2014 Michael Stapelberg
2 // vim:ts=4:sw=4:et
3 // Opens a WebSocket connection to Debian Code Search to send and receive
4 // search results almost instantanously.
6 // NB: All of these constants needs to match those in cmd/dcs-web/querymanager.go
7 var packagesPerPage = 5;
8 var resultsPerPackage = 2;
10 var animationFallback;
11 var showConnectProgress;
12 var tryHTTPS = true;
13 var websocket_url = window.location.protocol.replace('http', 'ws') + '//' + window.location.host + '/instantws';
14 var connection = new ReconnectingWebSocket(websocket_url);
15 var searchterm;
16 var queryDone = false;
18 // fatal (bool): Whether all ongoing operations should be cancelled.
20 // permanent (bool): Whether this message will be displayed permanently (e.g.
21 // “search results incomplete” vs. “trying to reconnect in 3s…”)
23 // unique_id (string): If non-null, only one message of this type will be
24 // displayed. Can be used to display only one notification about incomplete
25 // search results, regardless of how many backends the server returns as
26 // unhealthy.
28 // message (string): The human-readable error message.
29 function error(fatal, permanent, unique_id, message) {
30     if (unique_id !== null && $('#errors div[data-uniqueid=' + unique_id + ']').size() > 0) {
31         return;
32     }
33     if (fatal) {
34         progress(100, false, 'Error: ' + message);
35         return;
36     }
38     var div = $('<div class="alert alert-' + (permanent ? 'danger' : 'warning') + '" role="alert"></div>');
39     if (unique_id !== null) {
40         div.attr('data-uniqueid', unique_id);
41     }
42     div.text(message);
43     $('#errors').append(div);
44     return div;
47 // Setting percentage to 0 means initializing the progressbar. To display some
48 // sort of progress to the user, we’ll set it to 10%, so any actual progress
49 // that is communicated from the server will need to be ≥ 10%.
51 // Setting temporary to true will reset the text to the last non-temporary text
52 // upon completion (which is a call with percentage == 100).
53 function progress(percentage, temporary, text) {
54     fixProgressbar();
55     if (percentage == 0) {
56         $('#progressbar span').text(text);
57         $('#progressbar .progress-bar').css('width', '10%');
58         $('#progressbar .progress-bar').addClass('progress-active');
59         $('#progressbar').show();
60     } else {
61         if (text !== null) {
62             $('#progressbar span').text(text);
63             if (!temporary) {
64                 $('#progressbar').data('old-text', text);
65             }
66         }
67         $('#progressbar .progress-bar').css('width', percentage + '%');
68         if (percentage == 100) {
69             $('#progressbar .progress-bar').removeClass('progress-active');
70             if (temporary) {
71                 $('#progressbar span').text($('#progressbar').data('old-text'));
72             }
73         }
74     }
77 // Animates the search form from the middle of the page to the top right.
78 function animateSearchForm() {
79     // A bit hackish: we rip the search form out of the DOM and use
80     // position: absolute, so that we can later animate it across the page
81     // into the top right #searchbox div.
82     var sf = $('#searchform');
83     var pos = sf.position();
84     $('#searchbox .formplaceholder').css({ width: sf.width(), height: sf.height() });
85     pos.position = 'absolute';
86     $('#searchdiv .formplaceholder').css('height', sf.height());
87     sf.detach();
88     sf.appendTo('#content');
89     sf.css(pos);
91     sf.animate($('#searchbox').position(), 'fast', function() {
92         $('#searchdiv').hide();
93         // Unset focus
94         $('#searchform input[name=q]').blur();
95     });
98 function showResultsPage() {
99     $('#results li').remove();
100     $('#normalresults').show();
101     $('#progressbar').show();
102     $('#options').hide();
103     $('#packageshint').hide();
104     $('#pagination').text('');
105     $('#perpackage-pagination').text('');
108 function sendQuery() {
109     showResultsPage();
110     $('#packages').text('');
111     $('#errors div.alert-danger').remove();
112     var query = {
113         "Query": "q=" + encodeURIComponent(searchterm),
114     };
115     connection.send(JSON.stringify(query));
116     document.title = searchterm + ' · Debian Code Search';
117     var entries = localStorage.getItem("autocomplete");
118     if (entries === null) {
119         localStorage["autocomplete"] = JSON.stringify([searchterm]);
120     } else {
121         entries = JSON.parse(entries);
122         if (entries.indexOf(searchterm) === -1) {
123             entries.push(searchterm);
124         }
125         localStorage["autocomplete"] = JSON.stringify(entries);
126     }
127     animateSearchForm();
129     progress(0, false, 'Checking which files to grep…');
130     queryDone = false;
133 connection.onopen = function() {
134     clearTimeout(showConnectProgress);
135     $('#searchform input').attr('disabled', false);
137     // The URL dictates a search query, so start it.
138     if (!queryDone &&
139         (window.location.pathname.lastIndexOf('/results/', 0) === 0 ||
140          window.location.pathname.lastIndexOf('/perpackage-results/', 0) === 0)) {
141         var parts = new RegExp("results/([^/]+)").exec(window.location.pathname);
142         searchterm = decodeURIComponent(parts[1]);
143         sendQuery();
144     }
146     $('#searchform').off('submit').on('submit', function(ev) {
147         searchterm = $('#searchform input[name=q]').val();
148         sendQuery();
149         history.pushState({ searchterm: searchterm, nr: 0, perpkg: false }, 'page ' + 0, '/results/' + encodeURIComponent(searchterm) + '/page_0');
150         ev.preventDefault();
151     });
153     // This is triggered when the user navigates (e.g. via back button) between
154     // pages that were created using history.pushState().
155     $(window).off('popstate').on('popstate', function(ev) {
156         var state = ev.originalEvent.state;
157         if (state == null) {
158             // Restore the original page.
159             $('#normalresults, #perpackage, #progressbar, #errors, #packages, #options').hide();
160             $('#searchdiv').show();
161             $('#searchdiv .formplaceholder').after($('#searchform'));
162             $('#searchform').css('position', 'static');
163             restoreAutocomplete();
164         } else {
165             if (!$('#normalresults').is(':visible') &&
166                 !$('#perpackage').is(':visible')) {
167                 showResultsPage();
168                 animateSearchForm();
169                 // The following are necessary because we don’t send the query
170                 // anew and don’t get any progress messages (the final progress
171                 // message triggers displaying certain elements).
172                 $('#packages, #errors, #options').show();
173             }
174             $('#enable-perpackage').prop('checked', state.perpkg);
175             changeGrouping();
176             if (state.perpkg) {
177                 loadPerPkgPage(state.nr);
178             } else {
179                 loadPage(state.nr);
180             }
181         }
182     });
185 connection.onerror = function(e) {
186     // We could display an error, but since the page is supposed to fall back
187     // gracefully, why would the user be concerned if the search takes a tiny
188     // bit longer than usual?
189     // error(false, true, 'websocket_broken', 'Could not open WebSocket connection to ' + e.target.URL);
191     // Some transparent proxies don’t support WebSockets, e.g. Orange (big
192     // mobile provider in Switzerland) removes “Upgrade: ” headers from the
193     // HTTP requests. Therefore, we try to use wss:// if the connection was
194     // being made via ws://.
195     if (tryHTTPS) {
196         connection.url = websocket_url.replace('ws://', 'wss://');
197         tryHTTPS = false;
198     }
201 connection.onclose = function(e) {
202     // XXX: ideally, we’d only display the message if the reconnect takes longer than, say, a second?
203     var msg = error(false, false, null, 'Lost connection to Debian Code Search. Reconnecting…');
204     $('#searchform input').attr('disabled', true);
206     var oldHandler = connection.onopen;
207     connection.onopen = function() {
208         $('#searchform input').attr('disabled', false);
209         msg.remove();
210         connection.onopen = oldHandler;
211         oldHandler();
212     };
215 var queryid;
216 var resultpages;
217 var currentpage;
218 var currentpage_pkg;
219 var packages = [];
221 function addSearchResult(results, result) {
222     var context = [];
223     // NB: All of the following context lines are already HTML-escaped by the server.
224     context.push(result.Ctxp2);
225     context.push(result.Ctxp1);
226     context.push('<strong>' + result.Context + '</strong>');
227     context.push(result.Ctxn1);
228     context.push(result.Ctxn2);
229     // Remove any empty context lines (e.g. when the match is close to the
230     // beginning or end of the file).
231     context = $.grep(context, function(elm, idx) { return $.trim(elm) != ""; });
232     context = context.join("<br>").replace("\t", "    ");
234     // Split the path into source package (bold) and rest.
235     var delimiter = result.Path.indexOf("_");
236     var sourcePackage = result.Path.substring(0, delimiter);
237     var rest = result.Path.substring(delimiter);
239     // Append the new search result, then sort the results.
240     results.append('<li data-ranking="' + result.Ranking + '"><a href="/show?file=' + encodeURIComponent(result.Path) + '&line=' + result.Line + '"><code><strong>' + sourcePackage + '</strong>' + escapeForHTML(rest) + '</code></a><br><pre>' + context + '</pre><small>PathRank: ' + result.PathRank + ', Final: ' + result.Ranking + '</small></li>');
241     $('ul#results').append($('ul#results>li').detach().sort(function(a, b) {
242         return b.getAttribute('data-ranking') - a.getAttribute('data-ranking');
243     }));
245     // For performance reasons, we always keep the amount of displayed
246     // results at 10. With (typically rather generic) queries where the top
247     // results are changed very often, the page would get really slow
248     // otherwise.
249     var items = $('ul#results>li');
250     if (items.size() > 10) {
251         items.last().remove();
252     }
254     fixProgressbar();
257 function loadPage(nr) {
258     // Start the progress bar after 20ms. If the page was in the cache, this
259     // timer will be cancelled by the load callback below. If it wasn’t, 20ms
260     // is short enough of a delay to not be noticed by the user.
261     var progress_bar_start = setTimeout(function() {
262         progress(0, true, 'Loading search result page ' + (nr+1) + '…');
263     }, 20);
265     var pathname = '/results/' + encodeURIComponent(searchterm) + '/page_' + nr;
266     if (location.pathname != pathname) {
267         history.pushState({ searchterm: searchterm, nr: nr, perpkg: false }, 'page ' + nr, pathname);
268     }
269     $.ajax('/results/' + queryid + '/page_' + nr + '.json')
270         .done(function(data, textStatus, xhr) {
271             clearTimeout(progress_bar_start);
272             // TODO: experiment and see whether animating the results works
273             // well. Fade them in one after the other, see:
274             // http://www.google.com/design/spec/animation/meaningful-transitions.html#meaningful-transitions-hierarchical-timing
275             currentpage = nr;
276             updatePagination(currentpage, resultpages, false);
277             $('ul#results>li').remove();
278             var ul = $('ul#results');
279             $.each(data, function(idx, element) {
280                 addSearchResult(ul, element);
281             });
282             progress(100, true, null);
283         })
284         .fail(function(xhr, textStatus, errorThrown) {
285             error(true, true, null, 'Could not load search query results: ' + errorThrown);
286         });
289 // If preload is true, the current URL will not be updated, as the data is
290 // preloaded and inserted into (hidden) DOM elements.
291 function loadPerPkgPage(nr, preload) {
292     var progress_bar_start;
293     if (!preload) {
294         // Start the progress bar after 20ms. If the page was in the cache,
295         // this timer will be cancelled by the load callback below. If it
296         // wasn’t, 20ms is short enough of a delay to not be noticed by the
297         // user.
298         progress_bar_start = setTimeout(function() {
299             progress(0, true, 'Loading search result page ' + (nr+1) + '…');
300         }, 20);
301         var pathname = '/perpackage-results/' + encodeURIComponent(searchterm) + '/2/page_' + nr;
302         if (location.pathname != pathname) {
303             history.pushState({ searchterm: searchterm, nr: nr, perpkg: true }, 'page ' + nr, pathname);
304         }
305     }
306     $.ajax('/perpackage-results/' + queryid + '/2/page_' + nr + '.json')
307         .done(function(data, textStatus, xhr) {
308             if (progress_bar_start !== undefined) {
309                 clearTimeout(progress_bar_start);
310             }
311             currentpage_pkg = nr;
312             updatePagination(currentpage_pkg, Math.trunc(packages.length / packagesPerPage), true);
313             var pp = $('#perpackage-results');
314             pp.text('');
315             $.each(data, function(idx, meta) {
316                 pp.append('<h2>' + meta.Package + '</h2>');
317                 var ul = $('<ul></ul>');
318                 pp.append(ul);
319                 $.each(meta.Results, function(idx, result) {
320                     addSearchResult(ul, result);
321                 });
322                 var allResultsURL = '/results/' + encodeURIComponent(searchterm + ' package:' + meta.Package) + '/page_0';
323                 ul.append('<li><a href="' + allResultsURL + '">show all results in package <span class="packagename">' + meta.Package + '</span></a></li>');
324                 if (!preload) {
325                     progress(100, true, null);
326                 }
327             });
328         })
329         .fail(function(xhr, textStatus, errorThrown) {
330             error(true, true, null, 'Could not load search query results ("' + errorThrown + '").');
331         });
334 function pageUrl(page, perpackage) {
335     if (perpackage) {
336         return '/perpackage-results/' + encodeURIComponent(searchterm) + '/2/page_' + page;
337     } else {
338         return '/results/' + encodeURIComponent(searchterm) + '/page_' + page;
339     }
342 function updatePagination(currentpage, resultpages, perpackage) {
343     var clickFunc = (perpackage ? 'loadPerPkgPage' : 'loadPage');
344     var html = '<strong>Pages:</strong> ';
345     var start = Math.max(currentpage - 5, (currentpage > 0 ? 1 : 0));
346     var end = Math.min((currentpage >= 5 ? currentpage + 5 : 10), resultpages);
348     if (currentpage > 0) {
349         html += '<a href="' + pageUrl(currentpage-1, perpackage) + '" onclick="' + clickFunc + '(' + (currentpage-1) + ');return false;" rel="prev">&lt;</a> ';
350         html += '<a href="' + pageUrl(0, perpackage) + '" onclick="' + clickFunc + '(0);return false;">1</a> ';
351     }
353     if (start > 1) {
354         html += '… ';
355     }
357     for (var i = start; i < end; i++) {
358         html += '<a style="' + (i == currentpage ? "font-weight: bold" : "") + '" ' +
359                 'href="' + pageUrl(i, perpackage) + '" ' +
360                 'onclick="' + clickFunc + '(' + i + ');return false;">' + (i + 1) + '</a> ';
361     }
363     if (end < (resultpages-1)) {
364         html += '… ';
365     }
367     if (end < resultpages) {
368         html += '<a href="' + pageUrl(resultpages-1, perpackage) + '" onclick="' + clickFunc + '(' + (resultpages - 1) + ');return false;">' + resultpages + '</a>';
369     }
371     if (currentpage < (resultpages-1)) {
372         html += '<link rel="prerender" href="' + pageUrl(currentpage+1, perpackage) + '">';
373         html += '<a href="' + pageUrl(currentpage+1, perpackage) + '" onclick="' + clickFunc + '(' + (currentpage+1) + ');return false;" rel="next">&gt;</a> ';
374     }
376     $((perpackage ? '#perpackage-pagination' : '#pagination')).html(html);
379 function escapeForHTML(input) {
380     return $('<div/>').text(input).html();
383 connection.onmessage = function(e) {
384     var msg = JSON.parse(e.data);
385     switch (msg.Type) {
386         case "progress":
387         queryid = msg.QueryId;
389         progress(((msg.FilesProcessed / msg.FilesTotal) * 90) + 10,
390                  false,
391                  msg.FilesProcessed + ' / ' + msg.FilesTotal + ' files grepped (' + msg.Results + ' results)');
392         if (msg.FilesProcessed == msg.FilesTotal) {
393             queryDone = true;
394             if (msg.Results === 0) {
395                 progress(100, false, msg.FilesTotal + ' files grepped (' + msg.Results + ' results)');
396                 error(false, true, 'noresults', 'Your query “' + searchterm + '” had no results. Did you read the FAQ to make sure your syntax is correct?');
397             } else {
398                 $('#options').show();
400                 progress(100, false, msg.FilesTotal + ' files grepped (' + msg.Results + ' results)');
402                 // Request the results, but grouped by Debian source package.
403                 // Having these available means we can directly show them when the
404                 // user decides to switch to perpackage mode.
405                 loadPerPkgPage(0, true);
407                 $.ajax('/results/' + queryid + '/packages.json')
408                     .done(function(data, textStatus, xhr) {
409                         var p = $('#packages');
410                         p.text('');
411                         packages = data.Packages;
412                         updatePagination(currentpage_pkg, Math.trunc(packages.length / packagesPerPage), true);
413                         if (data.Packages.length === 1) {
414                             p.append('All results from Debian source package <strong>' + data.Packages[0] + '</strong>');
415                             $('#enable-perpackage').attr('disabled', 'disabled');
416                             $('label[for=enable-perpackage]').css('opacity', '0.5');
417                         } else if (data.Packages.length > 1) {
418                             // We are limiting the amount of packages because
419                             // some browsers (e.g. Chrome 40) will stop
420                             // displaying text with “white-space: nowrap” once
421                             // it becomes too long.
422                             var pkgLink = function(packageName) {
423                                 var url = '/results/' + encodeURIComponent(searchterm + ' package:' + packageName) + '/page_0';
424                                 return '<a href="' + url + '">' + packageName + '</a>';
425                             };
426                             var packagesList = data.Packages.slice(0, 1000).map(pkgLink).join(', ');
427                             p.append('<span><strong>Filter by package</strong>: ' + packagesList + '</span>');
428                             if ($('#packages span:first-child').prop('scrollWidth') > p.width()) {
429                                 p.append('<span class="showhint"><a href="#" onclick="$(\'#packageshint\').show(); return false;">▾</a></span>');
430                                 $('#packageshint').text('');
431                                 $('#packageshint').append('To see all packages which contain results: <pre>curl -s http://' + window.location.host + '/results/' + queryid + '/packages.json | jq -r \'.Packages[]\'</pre>');
432                             }
434                             $('#enable-perpackage').attr('disabled', null);
435                             $('label[for=enable-perpackage]').css('opacity', '1.0');
437                             if (window.location.pathname.lastIndexOf('/perpackage-results/', 0) === 0) {
438                                 var parts = new RegExp("/perpackage-results/([^/]+)/2/page_([0-9]+)").exec(window.location.pathname);
439                                 $('#enable-perpackage').prop('checked', true);
440                                 changeGrouping();
441                                 loadPerPkgPage(parseInt(parts[2]));
442                             }
443                         }
444                     })
445                     .fail(function(xhr, textStatus, errorThrown) {
446                         error(true, true, null, 'Loading search result package list failed: ' + errorThrown);
447                     });
448             }
449         }
450         break;
452         case "pagination":
453         // Store the values in global variables for constructing URLs when the
454         // user requests a different page.
455         resultpages = msg.ResultPages;
456         queryid = msg.QueryId;
457         currentpage = 0;
458         currentpage_pkg = 0;
459         updatePagination(currentpage, resultpages, false);
461         if (window.location.pathname.lastIndexOf('/results/', 0) === 0) {
462             var parts = new RegExp("/results/([^/]+)/page_([0-9]+)").exec(window.location.pathname);
463             loadPage(parseInt(parts[2]));
464         }
465         break;
467         case "result":
468         addSearchResult($('ul#results'), msg);
469         break;
471         case "error":
472         if (msg.ErrorType == "backendunavailable") {
473             error(false, true, msg.ErrorType, "The results may be incomplete, not all Debian Code Search servers are okay right now.");
474         } else {
475             error(msg.ErrorType);
476         }
477         break;
479         default:
480         throw new Error('Server sent unknown piece of data, type is "' + msg.Type);
481     }
484 function setPositionAbsolute(selector) {
485     var element = $(selector);
486     var pos = element.position();
487     pos.width = element.width();
488     pos.height = element.height();
489     pos.position = 'absolute';
490     element.css(pos);
493 function setPositionStatic(selector) {
494     $(selector).css({
495         'position': 'static',
496         'left': '',
497         'top': '',
498         'width': '',
499         'height': ''});
502 function animationSupported() {
503     var elm = $('#perpackage')[0];
504     var prefixes = ["webkit", "MS", "moz", "o", ""];
505     for (var i = 0; i < prefixes.length; i++) {
506         if (elm.style[prefixes[i] + 'AnimationName'] !== undefined) {
507             return true;
508         }
509     }
510     return false;
513 // Switch between displaying all results and grouping search results by Debian
514 // source package.
515 function changeGrouping() {
516     var ppelements = $('#perpackage');
518     var currentPerPkg = ppelements.is(':visible');
519     var shouldPerPkg = $('#enable-perpackage').prop('checked');
520     if (currentPerPkg === shouldPerPkg) {
521         return;
522     }
524     ppelements.data('hideAfterAnimation', !shouldPerPkg);
526     if (currentPerPkg) {
527         $('#perpackage').addClass('animation-reverse');
528     } else {
529         $('#perpackage').removeClass('animation-reverse');
530         $('#perpackage').show();
531     }
533     if (shouldPerPkg) {
534         ppelements.removeClass('animation-reverse');
535         var pathname = '/perpackage-results/' + encodeURIComponent(searchterm) + '/2/page_' + currentpage_pkg;
536         if (location.pathname != pathname) {
537             history.pushState(
538                 { searchterm: searchterm, nr: currentpage_pkg, perpkg: true },
539                 'page ' + currentpage_pkg,
540                 pathname);
541         }
543         setPositionAbsolute('#footer');
544         setPositionAbsolute('#normalresults');
545         $('#perpackage').show();
546     } else {
547         ppelements.addClass('animation-reverse');
548         var pathname = '/results/' + encodeURIComponent(searchterm) + '/page_' + currentpage;
549         if (location.pathname != pathname) {
550             history.pushState(
551                 { searchterm: searchterm, nr: currentpage, perpkg: false },
552                 'page ' + currentpage,
553                 pathname);
554         }
555         $('#normalresults').show();
556         // For browsers that don’t support animations, we need to have a fallback.
557         // The timer will be cancelled in the animationstart event handler.
558         if (!animationSupported()) {
559             animationFallback = setTimeout(function() {
560                 $('#perpackage').hide();
561                 setPositionStatic('#footer, #normalresults');
562             }, 100);
563         }
564     }
566     ppelements.removeClass('ppanimation');
567     // Trigger a reflow, otherwise removing/adding the animation class does not
568     // lead to restarting the animation.
569     ppelements[0].offsetWidth = ppelements[0].offsetWidth;
570     ppelements.addClass('ppanimation');
573 // Restore autocomplete from localstorage. This is necessary because the form
574 // never gets submitted (we intercept the submit event). All the alternatives
575 // are worse and have side-effects.
576 function restoreAutocomplete() {
577     var entries = localStorage.getItem("autocomplete");
578     if (entries !== null) {
579         entries = JSON.parse(entries);
580         var dataList = document.getElementById('autocomplete');
581         $('datalist').empty();
582         $.each(entries, function() {
583             var option = document.createElement('option');
584             option.value = this;
585             dataList.appendChild(option);
586         });
587     }
590 // This function needs to be called every time a scrollbar can appear (any DOM
591 // changes!) or the size of the window is changed.
593 // This is because span.progressbar-front-text needs to be the same width as
594 // div#progressbar, but there is no way to specify that in pure CSS :|.
595 function fixProgressbar() {
596     $('.progressbar-front-text').css('width', $('#progressbar').css('width'));
599 $(window).load(function() {
600     // Try to restore autocomplete settings even before the connection is
601     // established. If localStorage contains an entry, the user has used the
602     // instant search at least once, so chances are she’ll use it again.
603     restoreAutocomplete();
605     // Pressing “/” anywhere on the page focuses the search field.
606     $(document).keydown(function(e) {
607         if (e.which == 191) {
608             var q = $('#searchform input[name=q]');
609             if (q.is(':focus')) {
610                 return;
611             }
612             q.focus();
613             e.preventDefault();
614         }
615     });
617     fixProgressbar();
619     $(window).resize(fixProgressbar);
621     function bindAnimationEvent(element, name, cb) {
622         var prefixes = ["webkit", "MS", "moz", "o", ""];
623         for (var i = 0; i < prefixes.length; i++) {
624             if (i >= 3) {
625                 element.bind(prefixes[i] + name.toLowerCase(), cb);
626             } else {
627                 element.bind(prefixes[i] + name, cb);
628             }
629         }
630     }
632     var ppresults = $('#perpackage');
633     bindAnimationEvent(ppresults, 'AnimationStart', function(e) {
634         clearTimeout(animationFallback);
635     });
636     bindAnimationEvent(ppresults, 'AnimationEnd',  function(e) {
637         if (ppresults.data('hideAfterAnimation')) {
638             ppresults.hide();
639             setPositionStatic('#footer, #normalresults');
640         } else {
641             $('#normalresults').hide();
642         }
643     });
645     if (window.location.pathname.lastIndexOf('/results/', 0) === 0 ||
646         window.location.pathname.lastIndexOf('/perpackage-results/', 0) === 0) {
647         var parts = new RegExp("results/([^/]+)").exec(window.location.pathname);
648         $('#searchform input[name=q]').val(decodeURIComponent(parts[1]));
650         // If the websocket is not connected within 100ms, indicate progress.
651         if (connection.readyState != WebSocket.OPEN) {
652             $('#searchform input').attr('disabled', true);
653             showConnectProgress = setTimeout(function() {
654                 $('#progressbar').show();
655                 progress(0, true, 'Connecting…');
656             }, 100);
657         }
658     }