Scott Main | e4d8f1b | 2012-06-21 18:03:05 -0700 | [diff] [blame^] | 1 | /** |
| 2 | * jQuery history event v0.1 |
| 3 | * Copyright (c) 2008 Tom Rodenberg <tarodenberg gmail com> |
| 4 | * Licensed under the GPL (http://www.gnu.org/licenses/gpl.html) license. |
| 5 | */ |
| 6 | (function($) { |
| 7 | var currentHash, previousNav, timer, hashTrim = /^.*#/; |
| 8 | |
| 9 | var msie = { |
| 10 | iframe: null, |
| 11 | getDoc: function() { |
| 12 | return msie.iframe.contentWindow.document; |
| 13 | }, |
| 14 | getHash: function() { |
| 15 | return msie.getDoc().location.hash; |
| 16 | }, |
| 17 | setHash: function(hash) { |
| 18 | var d = msie.getDoc(); |
| 19 | d.open(); |
| 20 | d.close(); |
| 21 | d.location.hash = hash; |
| 22 | } |
| 23 | }; |
| 24 | |
| 25 | var historycheck = function() { |
| 26 | var hash = msie.iframe ? msie.getHash() : location.hash; |
| 27 | if (hash != currentHash) { |
| 28 | currentHash = hash; |
| 29 | if (msie.iframe) { |
| 30 | location.hash = currentHash; |
| 31 | } |
| 32 | var current = $.history.getCurrent(); |
| 33 | $.event.trigger('history', [current, previousNav]); |
| 34 | previousNav = current; |
| 35 | } |
| 36 | }; |
| 37 | |
| 38 | $.history = { |
| 39 | add: function(hash) { |
| 40 | hash = '#' + hash.replace(hashTrim, ''); |
| 41 | if (currentHash != hash) { |
| 42 | var previous = $.history.getCurrent(); |
| 43 | location.hash = currentHash = hash; |
| 44 | if (msie.iframe) { |
| 45 | msie.setHash(currentHash); |
| 46 | } |
| 47 | $.event.trigger('historyadd', [$.history.getCurrent(), previous]); |
| 48 | } |
| 49 | if (!timer) { |
| 50 | timer = setInterval(historycheck, 100); |
| 51 | } |
| 52 | }, |
| 53 | getCurrent: function() { |
| 54 | if (currentHash) { |
| 55 | return currentHash.replace(hashTrim, ''); |
| 56 | } else { |
| 57 | return ""; |
| 58 | } |
| 59 | } |
| 60 | }; |
| 61 | |
| 62 | $.fn.history = function(fn) { |
| 63 | $(this).bind('history', fn); |
| 64 | }; |
| 65 | |
| 66 | $.fn.historyadd = function(fn) { |
| 67 | $(this).bind('historyadd', fn); |
| 68 | }; |
| 69 | |
| 70 | $(function() { |
| 71 | currentHash = location.hash; |
| 72 | if ($.browser.msie) { |
| 73 | msie.iframe = $('<iframe style="display:none"src="javascript:false;"></iframe>') |
| 74 | .prependTo('body')[0]; |
| 75 | msie.setHash(currentHash); |
| 76 | currentHash = msie.getHash(); |
| 77 | } |
| 78 | }); |
| 79 | })(jQuery); |
| 80 | |
| 81 | |
| 82 | |
| 83 | |
| 84 | |
| 85 | |
| 86 | |
| 87 | |
| 88 | |
| 89 | |
| 90 | |
| 91 | |
| 92 | var gSelectedIndex = -1; |
| 93 | var gSelectedID = -1; |
| 94 | var gMatches = new Array(); |
| 95 | var gLastText = ""; |
| 96 | var ROW_COUNT = 20; |
| 97 | var gInitialized = false; |
| 98 | |
| 99 | function set_item_selected($li, selected) |
| 100 | { |
| 101 | if (selected) { |
| 102 | $li.attr('class','jd-autocomplete jd-selected'); |
| 103 | } else { |
| 104 | $li.attr('class','jd-autocomplete'); |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | function set_item_values(toroot, $li, match) |
| 109 | { |
| 110 | var $link = $('a',$li); |
| 111 | $link.html(match.__hilabel || match.label); |
| 112 | $link.attr('href',toroot + match.link); |
| 113 | } |
| 114 | |
| 115 | function sync_selection_table(toroot) |
| 116 | { |
| 117 | var $list = $("#search_filtered"); |
| 118 | var $li; //list item jquery object |
| 119 | var i; //list item iterator |
| 120 | gSelectedID = -1; |
| 121 | |
| 122 | //initialize the table; draw it for the first time (but not visible). |
| 123 | if (!gInitialized) { |
| 124 | for (i=0; i<ROW_COUNT; i++) { |
| 125 | var $li = $("<li class='jd-autocomplete'></li>"); |
| 126 | $list.append($li); |
| 127 | |
| 128 | $li.mousedown(function() { |
| 129 | window.location = this.firstChild.getAttribute("href"); |
| 130 | }); |
| 131 | $li.mouseover(function() { |
| 132 | $('#search_filtered li').removeClass('jd-selected'); |
| 133 | $(this).addClass('jd-selected'); |
| 134 | gSelectedIndex = $('#search_filtered li').index(this); |
| 135 | }); |
| 136 | $li.append('<a></a>'); |
| 137 | } |
| 138 | gInitialized = true; |
| 139 | } |
| 140 | |
| 141 | //if we have results, make the table visible and initialize result info |
| 142 | if (gMatches.length > 0) { |
| 143 | $('#search_filtered_div').removeClass('no-display'); |
| 144 | var N = gMatches.length < ROW_COUNT ? gMatches.length : ROW_COUNT; |
| 145 | for (i=0; i<N; i++) { |
| 146 | $li = $('#search_filtered li:nth-child('+(i+1)+')'); |
| 147 | $li.attr('class','show-item'); |
| 148 | set_item_values(toroot, $li, gMatches[i]); |
| 149 | set_item_selected($li, i == gSelectedIndex); |
| 150 | if (i == gSelectedIndex) { |
| 151 | gSelectedID = gMatches[i].id; |
| 152 | } |
| 153 | } |
| 154 | //start hiding rows that are no longer matches |
| 155 | for (; i<ROW_COUNT; i++) { |
| 156 | $li = $('#search_filtered li:nth-child('+(i+1)+')'); |
| 157 | $li.attr('class','no-display'); |
| 158 | } |
| 159 | //if there are more results we're not showing, so say so. |
| 160 | /* if (gMatches.length > ROW_COUNT) { |
| 161 | li = list.rows[ROW_COUNT]; |
| 162 | li.className = "show-item"; |
| 163 | c1 = li.cells[0]; |
| 164 | c1.innerHTML = "plus " + (gMatches.length-ROW_COUNT) + " more"; |
| 165 | } else { |
| 166 | list.rows[ROW_COUNT].className = "hide-item"; |
| 167 | }*/ |
| 168 | //if we have no results, hide the table |
| 169 | } else { |
| 170 | $('#search_filtered_div').addClass('no-display'); |
| 171 | } |
| 172 | } |
| 173 | |
| 174 | function search_changed(e, kd, toroot) |
| 175 | { |
| 176 | var search = document.getElementById("search_autocomplete"); |
| 177 | var text = search.value.replace(/(^ +)|( +$)/g, ''); |
| 178 | |
| 179 | // show/hide the close button |
| 180 | if (text != '') { |
| 181 | $(".search .close").removeClass("hide"); |
| 182 | } else { |
| 183 | $(".search .close").addClass("hide"); |
| 184 | } |
| 185 | |
| 186 | // 13 = enter |
| 187 | if (e.keyCode == 13) { |
| 188 | $('#search_filtered_div').addClass('no-display'); |
| 189 | if (!$('#search_filtered_div').hasClass('no-display') || (gSelectedIndex < 0)) { |
| 190 | return true; |
| 191 | } else if (kd && gSelectedIndex >= 0) { |
| 192 | window.location = toroot + gMatches[gSelectedIndex].link; |
| 193 | return false; |
| 194 | } |
| 195 | } |
| 196 | // 38 -- arrow up |
| 197 | else if (kd && (e.keyCode == 38)) { |
| 198 | if (gSelectedIndex >= 0) { |
| 199 | $('#search_filtered li').removeClass('jd-selected'); |
| 200 | gSelectedIndex--; |
| 201 | $('#search_filtered li:nth-child('+(gSelectedIndex+1)+')').addClass('jd-selected'); |
| 202 | } |
| 203 | return false; |
| 204 | } |
| 205 | // 40 -- arrow down |
| 206 | else if (kd && (e.keyCode == 40)) { |
| 207 | if (gSelectedIndex < gMatches.length-1 |
| 208 | && gSelectedIndex < ROW_COUNT-1) { |
| 209 | $('#search_filtered li').removeClass('jd-selected'); |
| 210 | gSelectedIndex++; |
| 211 | $('#search_filtered li:nth-child('+(gSelectedIndex+1)+')').addClass('jd-selected'); |
| 212 | } |
| 213 | return false; |
| 214 | } |
| 215 | else if (!kd && (e.keyCode != 40) && (e.keyCode != 38)) { |
| 216 | gMatches = new Array(); |
| 217 | matchedCount = 0; |
| 218 | gSelectedIndex = -1; |
| 219 | for (var i=0; i<DATA.length; i++) { |
| 220 | var s = DATA[i]; |
| 221 | if (text.length != 0 && |
| 222 | s.label.toLowerCase().indexOf(text.toLowerCase()) != -1) { |
| 223 | gMatches[matchedCount] = s; |
| 224 | matchedCount++; |
| 225 | } |
| 226 | } |
| 227 | rank_autocomplete_results(text); |
| 228 | for (var i=0; i<gMatches.length; i++) { |
| 229 | var s = gMatches[i]; |
| 230 | if (gSelectedID == s.id) { |
| 231 | gSelectedIndex = i; |
| 232 | } |
| 233 | } |
| 234 | highlight_autocomplete_result_labels(text); |
| 235 | sync_selection_table(toroot); |
| 236 | return true; // allow the event to bubble up to the search api |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | function rank_autocomplete_results(query) { |
| 241 | query = query || ''; |
| 242 | if (!gMatches || !gMatches.length) |
| 243 | return; |
| 244 | |
| 245 | // helper function that gets the last occurence index of the given regex |
| 246 | // in the given string, or -1 if not found |
| 247 | var _lastSearch = function(s, re) { |
| 248 | if (s == '') |
| 249 | return -1; |
| 250 | var l = -1; |
| 251 | var tmp; |
| 252 | while ((tmp = s.search(re)) >= 0) { |
| 253 | if (l < 0) l = 0; |
| 254 | l += tmp; |
| 255 | s = s.substr(tmp + 1); |
| 256 | } |
| 257 | return l; |
| 258 | }; |
| 259 | |
| 260 | // helper function that counts the occurrences of a given character in |
| 261 | // a given string |
| 262 | var _countChar = function(s, c) { |
| 263 | var n = 0; |
| 264 | for (var i=0; i<s.length; i++) |
| 265 | if (s.charAt(i) == c) ++n; |
| 266 | return n; |
| 267 | }; |
| 268 | |
| 269 | var queryLower = query.toLowerCase(); |
| 270 | var queryAlnum = (queryLower.match(/\w+/) || [''])[0]; |
| 271 | var partPrefixAlnumRE = new RegExp('\\b' + queryAlnum); |
| 272 | var partExactAlnumRE = new RegExp('\\b' + queryAlnum + '\\b'); |
| 273 | |
| 274 | var _resultScoreFn = function(result) { |
| 275 | // scores are calculated based on exact and prefix matches, |
| 276 | // and then number of path separators (dots) from the last |
| 277 | // match (i.e. favoring classes and deep package names) |
| 278 | var score = 1.0; |
| 279 | var labelLower = result.label.toLowerCase(); |
| 280 | var t; |
| 281 | t = _lastSearch(labelLower, partExactAlnumRE); |
| 282 | if (t >= 0) { |
| 283 | // exact part match |
| 284 | var partsAfter = _countChar(labelLower.substr(t + 1), '.'); |
| 285 | score *= 200 / (partsAfter + 1); |
| 286 | } else { |
| 287 | t = _lastSearch(labelLower, partPrefixAlnumRE); |
| 288 | if (t >= 0) { |
| 289 | // part prefix match |
| 290 | var partsAfter = _countChar(labelLower.substr(t + 1), '.'); |
| 291 | score *= 20 / (partsAfter + 1); |
| 292 | } |
| 293 | } |
| 294 | |
| 295 | return score; |
| 296 | }; |
| 297 | |
| 298 | for (var i=0; i<gMatches.length; i++) { |
| 299 | gMatches[i].__resultScore = _resultScoreFn(gMatches[i]); |
| 300 | } |
| 301 | |
| 302 | gMatches.sort(function(a,b){ |
| 303 | var n = b.__resultScore - a.__resultScore; |
| 304 | if (n == 0) // lexicographical sort if scores are the same |
| 305 | n = (a.label < b.label) ? -1 : 1; |
| 306 | return n; |
| 307 | }); |
| 308 | } |
| 309 | |
| 310 | function highlight_autocomplete_result_labels(query) { |
| 311 | query = query || ''; |
| 312 | if (!gMatches || !gMatches.length) |
| 313 | return; |
| 314 | |
| 315 | var queryLower = query.toLowerCase(); |
| 316 | var queryAlnumDot = (queryLower.match(/[\w\.]+/) || [''])[0]; |
| 317 | var queryRE = new RegExp( |
| 318 | '(' + queryAlnumDot.replace(/\./g, '\\.') + ')', 'ig'); |
| 319 | for (var i=0; i<gMatches.length; i++) { |
| 320 | gMatches[i].__hilabel = gMatches[i].label.replace( |
| 321 | queryRE, '<b>$1</b>'); |
| 322 | } |
| 323 | } |
| 324 | |
| 325 | function search_focus_changed(obj, focused) |
| 326 | { |
| 327 | if (!focused) { |
| 328 | if(obj.value == ""){ |
| 329 | $(".search .close").addClass("hide"); |
| 330 | } |
| 331 | document.getElementById("search_filtered_div").className = "no-display"; |
| 332 | } |
| 333 | } |
| 334 | |
| 335 | function submit_search() { |
| 336 | var query = document.getElementById('search_autocomplete').value; |
| 337 | location.hash = 'q=' + query; |
| 338 | $.history.add('q=' + query); |
| 339 | loadSearchResults(); |
| 340 | $("#searchResults").slideDown(); |
| 341 | return false; |
| 342 | } |
| 343 | |
| 344 | |
| 345 | function hideResults() { |
| 346 | $("#searchResults").slideUp(); |
| 347 | $(".search .close").addClass("hide"); |
| 348 | location.hash = ''; |
| 349 | drawOptions.setInput(document.getElementById("searchResults")); |
| 350 | |
| 351 | $("#search_autocomplete").blur(); |
| 352 | return false; |
| 353 | } |
| 354 | |
| 355 | |
| 356 | |
| 357 | |
| 358 | |
| 359 | |
| 360 | |
| 361 | |
| 362 | |
| 363 | |
| 364 | |
| 365 | |
| 366 | /************ SEARCH ENGINE ***************/ |
| 367 | |
| 368 | |
| 369 | google.load('search', '1'); |
| 370 | |
| 371 | function loadSearchResults() { |
| 372 | if (location.hash.indexOf("q=") == -1) { |
| 373 | // if there's no query in the url, don't search and make sure results are hidden |
| 374 | $('#searchResults').hide(); |
| 375 | return; |
| 376 | } |
| 377 | |
| 378 | var $results = $("#searchResults"); |
| 379 | if ($results.is(":hidden")) { |
| 380 | $results.slideDown(); |
| 381 | } |
| 382 | |
| 383 | document.getElementById("search_autocomplete").style.color = "#000"; |
| 384 | |
| 385 | // create search control |
| 386 | searchControl = new google.search.SearchControl(); |
| 387 | |
| 388 | // use our existing search form and use tabs when multiple searchers are used |
| 389 | drawOptions = new google.search.DrawOptions(); |
| 390 | drawOptions.setDrawMode(google.search.SearchControl.DRAW_MODE_TABBED); |
| 391 | drawOptions.setInput(document.getElementById("search_autocomplete")); |
| 392 | |
| 393 | // configure search result options |
| 394 | searchOptions = new google.search.SearcherOptions(); |
| 395 | searchOptions.setExpandMode(GSearchControl.EXPAND_MODE_OPEN); |
| 396 | |
| 397 | // configure each of the searchers, for each tab |
| 398 | devSiteSearcher = new google.search.WebSearch(); |
| 399 | devSiteSearcher.setUserDefinedLabel("All"); |
| 400 | devSiteSearcher.setSiteRestriction("001482626316274216503:zu90b7s047u"); |
| 401 | |
| 402 | designSearcher = new google.search.WebSearch(); |
| 403 | designSearcher.setUserDefinedLabel("Design"); |
| 404 | designSearcher.setSiteRestriction("http://developer.android.com/design/"); |
| 405 | |
| 406 | trainingSearcher = new google.search.WebSearch(); |
| 407 | trainingSearcher.setUserDefinedLabel("Training"); |
| 408 | trainingSearcher.setSiteRestriction("http://developer.android.com/training/"); |
| 409 | |
| 410 | guidesSearcher = new google.search.WebSearch(); |
| 411 | guidesSearcher.setUserDefinedLabel("Guides"); |
| 412 | guidesSearcher.setSiteRestriction("http://developer.android.com/guide/"); |
| 413 | |
| 414 | referenceSearcher = new google.search.WebSearch(); |
| 415 | referenceSearcher.setUserDefinedLabel("Reference"); |
| 416 | referenceSearcher.setSiteRestriction("http://developer.android.com/reference/"); |
| 417 | |
| 418 | blogSearcher = new google.search.WebSearch(); |
| 419 | blogSearcher.setUserDefinedLabel("Blog"); |
| 420 | blogSearcher.setSiteRestriction("http://android-developers.blogspot.com"); |
| 421 | |
| 422 | // add each searcher to the search control |
| 423 | searchControl.addSearcher(devSiteSearcher, searchOptions); |
| 424 | searchControl.addSearcher(designSearcher, searchOptions); |
| 425 | searchControl.addSearcher(trainingSearcher, searchOptions); |
| 426 | searchControl.addSearcher(guidesSearcher, searchOptions); |
| 427 | searchControl.addSearcher(referenceSearcher, searchOptions); |
| 428 | searchControl.addSearcher(blogSearcher, searchOptions); |
| 429 | |
| 430 | // configure result options |
| 431 | searchControl.setResultSetSize(google.search.Search.LARGE_RESULTSET); |
| 432 | searchControl.setLinkTarget(google.search.Search.LINK_TARGET_SELF); |
| 433 | searchControl.setTimeoutInterval(google.search.SearchControl.TIMEOUT_LONG); |
| 434 | searchControl.setNoResultsString(google.search.SearchControl.NO_RESULTS_DEFAULT_STRING); |
| 435 | |
| 436 | // upon ajax search, refresh the url and search title |
| 437 | searchControl.setSearchStartingCallback(this, function(control, searcher, query) { |
| 438 | updateResultTitle(query); |
| 439 | var query = document.getElementById('search_autocomplete').value; |
| 440 | location.hash = 'q=' + query; |
| 441 | $.history.add('q=' + query); |
| 442 | }); |
| 443 | |
| 444 | // draw the search results box |
| 445 | searchControl.draw(document.getElementById("leftSearchControl"), drawOptions); |
| 446 | |
| 447 | // get query and execute the search |
| 448 | searchControl.execute(decodeURI(getQuery(location.hash))); |
| 449 | |
| 450 | document.getElementById("search_autocomplete").focus(); |
| 451 | addTabListeners(); |
| 452 | } |
| 453 | // End of loadSearchResults |
| 454 | |
| 455 | |
| 456 | google.setOnLoadCallback(loadSearchResults, true); |
| 457 | |
| 458 | // when an event on the browser history occurs (back, forward, load) perform a search |
| 459 | $(window).history(function(e, hash) { |
| 460 | var query = decodeURI(getQuery(hash)); |
| 461 | if (query == "undefined") { |
| 462 | hideResults(); |
| 463 | return; |
| 464 | } |
| 465 | searchControl.execute(query); |
| 466 | |
| 467 | updateResultTitle(query); |
| 468 | }); |
| 469 | |
| 470 | function updateResultTitle(query) { |
| 471 | $("#searchTitle").html("Results for <em>" + escapeHTML(query) + "</em>"); |
| 472 | } |
| 473 | |
| 474 | // forcefully regain key-up event control (previously jacked by search api) |
| 475 | $("#search_autocomplete").keyup(function(event) { |
| 476 | return search_changed(event, false, '/'); |
| 477 | }); |
| 478 | |
| 479 | // add event listeners to each tab so we can track the browser history |
| 480 | function addTabListeners() { |
| 481 | var tabHeaders = $(".gsc-tabHeader"); |
| 482 | for (var i = 0; i < tabHeaders.length; i++) { |
| 483 | $(tabHeaders[i]).attr("id",i).click(function() { |
| 484 | /* |
| 485 | // make a copy of the page numbers for the search left pane |
| 486 | setTimeout(function() { |
| 487 | // remove any residual page numbers |
| 488 | $('#searchResults .gsc-tabsArea .gsc-cursor-box.gs-bidi-start-align').remove(); |
| 489 | // move the page numbers to the left position; make a clone, |
| 490 | // because the element is drawn to the DOM only once |
| 491 | // and because we're going to remove it (previous line), |
| 492 | // we need it to be available to move again as the user navigates |
| 493 | $('#searchResults .gsc-webResult .gsc-cursor-box.gs-bidi-start-align:visible') |
| 494 | .clone().appendTo('#searchResults .gsc-tabsArea'); |
| 495 | }, 200); |
| 496 | */ |
| 497 | }); |
| 498 | } |
| 499 | setTimeout(function(){$(tabHeaders[0]).click()},200); |
| 500 | } |
| 501 | |
| 502 | |
| 503 | function getQuery(hash) { |
| 504 | var queryParts = hash.split('='); |
| 505 | return queryParts[1]; |
| 506 | } |
| 507 | |
| 508 | /* returns the given string with all HTML brackets converted to entities |
| 509 | TODO: move this to the site's JS library */ |
| 510 | function escapeHTML(string) { |
| 511 | return string.replace(/</g,"<") |
| 512 | .replace(/>/g,">"); |
| 513 | } |
| 514 | |
| 515 | |