1/* 2 AJAX Suggestions is developed by Robert Nyman, http://www.robertnyman.com, and it is released according to the 3 Creative Commons Deed license (http://creativecommons.org/licenses/GPL/2.0/) 4 For more information, please see http://www.robertnyman.com/ajax-suggestions 5*/ 6// --- 7var ajaxSuggestions = { 8 // Settings 9 elmIdToPresentResultsIn : "search-results", 10 elmIdResultsContainer : "search-result-suggestions", 11 charactersBeforeSearch : 2, 12 timeBeforeSuggest : 200, // In milliseconds 13 sameWidthAsInputElm : false, 14 offsetLeft: 0, 15 offsetTop : 0, 16 urlExt : "search=", 17 addSearchTermToQueryString : true, 18 addKeyNavigationEvents : true, 19 hideResultsOnDocumentClick : true, 20 itemClassName : "item", 21 itemSelectedClassName : "selected", 22 itemInsertValueIntoInputClassName : "choose-value", 23 itemInsertValueSetFocusToInput : true, 24 hideResultsWhenInsertValueIsSelected : true, 25 itemSeparator : ";", 26 turnAutoCompleteOff : true, 27 // Object properties 28 xmlHttp : null, 29 elements : [], 30 timer : null, 31 currentElm : null, 32 currentKeyEvent : null, 33 suggestionsForElm : null, 34 elmToPresentResultsIn : null, 35 elmResultsContainer : null, 36 suggestions : [], 37 resultIndex : 0, 38 selectedItem : -1, 39 resultsAreVisible : false, 40 valueAddedFromResultsListToInput : false, 41 42 init : function (){ 43 this.xmlHttp = this.createXmlHttp(); 44 if(this.xmlHttp){ 45 if(typeof document.getElementsByClassName != "function"){ 46 document.getElementsByClassName = this.elmByClass; 47 } 48 this.elements = document.getElementsByClassName("ajax-suggestion", "input"); 49 this.applyEvents(); 50 this.elmToPresentResultsIn = document.getElementById(this.elmIdToPresentResultsIn); 51 this.elmResultsContainer = document.getElementById(this.elmIdResultsContainer); 52 if(this.addKeyNavigationEvents){ 53 this.addEvent(document, "keydown", this.preventDefaultForArrowKeys); 54 this.addEvent(document, "keypress", this.preventDefaultForArrowKeys); 55 this.addEvent(document, "keyup", this.navigateResults); 56 } 57 if(this.hideResultsOnDocumentClick){ 58 this.addEvent(document, "click", this.clearResultsElement); 59 } 60 } 61 }, 62 63 createXmlHttp : function (){ 64 this.xmlHttp = null; 65 if(typeof XMLHttpRequest != "undefined"){ 66 this.xmlHttp = new XMLHttpRequest(); 67 } 68 else if(typeof window.ActiveXObject != "undefined"){ 69 try { 70 this.xmlHttp = new ActiveXObject("Msxml2.XMLHTTP.4.0"); 71 } 72 catch(e){ 73 try { 74 this.xmlHttp = new ActiveXObject("MSXML2.XMLHTTP"); 75 } 76 catch(e){ 77 try { 78 this.xmlHttp = new ActiveXObject("Microsoft.XMLHTTP"); 79 } 80 catch(e){ 81 this.xmlHttp = null; 82 } 83 } 84 } 85 } 86 return this.xmlHttp; 87 }, 88 89 applyEvents : function (){ 90 var element; 91 for(var i=0; i<this.elements.length; i++){ 92 element = this.elements[i]; 93 if(this.turnAutoCompleteOff){ 94 element.setAttribute("autocomplete", "off"); 95 } 96 this.addEvent(element, "keyup", this.startSuggestionsTimer); 97 if(this.hideResultsOnDocumentClick){ 98 this.addEvent(element, "click", this.preventInputClickBubbling); 99 } 100 } 101 }, 102 103 startSuggestionsTimer : function (evt){ 104 clearTimeout(ajaxSuggestions.timer); 105 ajaxSuggestions.currentElm = (/input/i.test(this.nodeName))? this : evt.srcElement; 106 ajaxSuggestions.currentKeyEvent = evt.keyCode; 107 ajaxSuggestions.timer = setTimeout("ajaxSuggestions.getSuggestions()", ajaxSuggestions.timeBeforeSuggest); 108 }, 109 110 getSuggestions : function (){ 111 var value = this.currentElm.value; 112 if(!/13|27|37|39/.test(this.currentKeyEvent)){ 113 var url = this.currentElm.className.replace(/.*url-([\w\/\?\.-]+).*/, "$1"); 114 if(!this.valueAddedFromResultsListToInput){ 115 ajaxSuggestions.clearResults(true); 116 } 117 if(value.length > this.charactersBeforeSearch && url.length > 0){ 118 this.makeSuggestionCall(value, url); 119 } 120 else if(value.length == 0 || !this.valueAddedFromResultsListToInput){ 121 ajaxSuggestions.clearResults(); 122 } 123 } 124 }, 125 126 makeSuggestionCall : function (value, url){ 127 var regExpValue = new RegExp(("^" + value + "$"), "i"); 128 var exists = false; 129 var suggestionItem; 130 var url = url + ((/\?/.test(url))? "&" : "?") + this.urlExt + ((this.addSearchTermToQueryString)? value : ""); 131 for(var i=0; i<this.suggestions.length; i++){ 132 suggestionItem = this.suggestions[i]; 133 if(regExpValue.test(suggestionItem[0]) && url == suggestionItem[2]){ 134 exists = true; 135 this.resultIndex = i; 136 this.presentResult(this.suggestions[i][1]); 137 break; 138 } 139 }; 140 if(!exists){ 141 this.xmlHttp.onreadystatechange = function (){}; 142 this.xmlHttp.abort(); 143 this.currentValue = value; 144 this.currentURL = url; 145 this.xmlHttp.open("GET", url, true); 146 this.xmlHttp.onreadystatechange = this.getResults; 147 this.xmlHttp.send(null); 148 } 149 }, 150 151 getResults : function (){ 152 if(ajaxSuggestions.xmlHttp.readyState == 4 && ajaxSuggestions.xmlHttp.responseText.length > 0){ 153 ajaxSuggestions.loadResults(); 154 } 155 }, 156 157 loadResults : function (){ 158 this.resultIndex = this.suggestions.length; 159 this.suggestions.push([this.currentValue, this.xmlHttp.responseText, this.currentURL]); 160 this.presentResult(); 161 }, 162 163 presentResult : function (){ 164 this.elmToPresentResultsIn.innerHTML = this.suggestions[this.resultIndex][1]; 165 var coordinates = this.getCoordinates();
166 var elm = this.elmResultsContainer.style; 167 elm.left = coordinates[0] + this.offsetLeft + "px"; 168 elm.top = coordinates[1] + this.currentElm.offsetHeight + this.offsetTop + "px"; 169 if(this.sameWidthAsInputElm){ 170 elm.width = this.currentElm.offsetWidth + "px"; 171 } 172 this.applyResultEvents(); 173 elm.display = "block"; 174 this.resultsAreVisible = true; 175 if(this.addKeyNavigationEvents && /38|40/.test(this.currentKeyEvent)){ 176 if(!this.valueAddedFromResultsListToInput){ 177 this.selectedItem = -1; 178 } 179 this.navigateResults(null, this.currentKeyEvent); 180 } 181 }, 182 183 clearResults : function (justClear){ 184 if(this.elmResultsContainer && this.elmToPresentResultsIn){ 185 if(!justClear){ 186 this.elmResultsContainer.style.display = "none"; 187 this.resultsAreVisible = false; 188 } 189 this.elmToPresentResultsIn.innerHTML = ""; 190 this.selectedItem = -1; 191 } 192 }, 193 194 clearResultsElement : function (){ 195 ajaxSuggestions.clearResults(); 196 }, 197 198 navigateResults : function (evt, keyCode){ 199 if(ajaxSuggestions.currentElm && ajaxSuggestions.elmToPresentResultsIn){ 200 var event = (typeof evt != "undefined")? evt : event; 201 if(typeof ajaxSuggestions.elmToPresentResultsIn.getElementsByClassName != "function"){ 202 ajaxSuggestions.elmToPresentResultsIn.getElementsByClassName = ajaxSuggestions.elmByClass; 203 } 204 var results = ajaxSuggestions.elmToPresentResultsIn.getElementsByClassName(ajaxSuggestions.itemClassName); 205 var selectedItem = (!evt && keyCode == 38)? results.length : ajaxSuggestions.selectedItem; 206 var keyCode = keyCode || event.keyCode; 207 var navigateUp = keyCode == 37 || keyCode == 38; 208 var navigateDown = keyCode == 39 || keyCode == 40; 209 if(results.length > 0 && (navigateUp || navigateDown)){ 210 if(navigateUp){ 211 if((selectedItem - 1) >= 0){ 212 selectedItem--; 213 } 214 else{ 215 selectedItem = -1; 216 } 217 } 218 else if(navigateDown){ 219 if((selectedItem + 1) < results.length){ 220 selectedItem++; 221 } 222 else{ 223 selectedItem = -1; 224 } 225 } 226 var item; 227 var classToRemove; 228 for(var i=0; i<results.length; i++){ 229 item = results[i]; 230 classToRemove = new RegExp((ajaxSuggestions.itemSelectedClassName + "\s?"), "i"); 231 item.className = item.className.replace(classToRemove, "").replace(/^\s?|\s?$/g, ""); 232 }; 233 ajaxSuggestions.selectedItem = selectedItem; 234 var elmToFocus = ajaxSuggestions.currentElm; 235 if(selectedItem > -1){ 236 var currentItem = results[selectedItem]; 237 var currentClass = item.className; 238 if(!new RegExp(ajaxSuggestions.itemSelectedClassName, "i").test(currentClass)){ 239 currentItem.className = currentClass + ((currentClass.length > 0)? " " : "") + ajaxSuggestions.itemSelectedClassName; 240 } 241 elmToFocus = results[selectedItem]; 242 } 243 try{ 244 elmToFocus.focus(); 245 } 246 catch(e){ 247 // Just in case... :-) 248 } 249 if(event){ 250 if(event.preventDefault){ 251 event.preventDefault(); 252 } 253 else{ 254 event.returnValue = false; 255 } 256 if(event.stopPropagation){ 257 event.stopPropagation(); 258 } 259 else{ 260 event.cancelBubble = true; 261 } 262 } 263 return false; 264 } 265 else if(keyCode == 27){ 266 ajaxSuggestions.clearResults(); 267 try{ 268 ajaxSuggestions.currentElm.focus(); 269 } 270 catch(e){ 271 // Just in case... :-) 272 } 273 } 274 } 275 }, 276 277 applyResultEvents : function (){ 278 if(typeof this.elmToPresentResultsIn.getElementsByClassName != "function"){ 279 this.elmToPresentResultsIn.getElementsByClassName = this.elmByClass; 280 } 281 var insertValueItems = this.elmToPresentResultsIn.getElementsByClassName(this.itemInsertValueIntoInputClassName, "a"); 282 var item; 283 for(var i=0; i<insertValueItems.length; i++){ 284 item = insertValueItems[i]; 285 item.inputRef = this.currentElm; 286 this.addEvent(item, "click", this.insertValueIntoField); 287 }; 288 }, 289 290 insertValueIntoField : function (evt){ 291 var elm = (/a/i.test(this.nodeName))? this : evt.srcElement; 292 var input = elm.inputRef; 293 var value = elm.getAttribute("href"); 294 if(!new RegExp(value).test(input.value)){ 295 input.value = ((input.value.length > 0 && /;/i.test(input.value))? (input.value + value) : value) + ajaxSuggestions.itemSeparator; 296 } 297 if(evt.preventDefault){ 298 evt.preventDefault(); 299 } 300 else{ 301 evt.returnValue = false; 302 } 303 if(evt.stopPropagation){ 304 evt.stopPropagation(); 305 } 306 else{ 307 evt.cancelBubble = true; 308 } 309 if(ajaxSuggestions.itemInsertValueSetFocusToInput){ 310 try{ 311 input.focus(); 312 } 313 catch(e){ 314 // Just in case... :-) 315 } 316 } 317 if(ajaxSuggestions.hideResultsWhenInsertValueIsSelected){ 318 ajaxSuggestions.clearResults(); 319 } 320 ajaxSuggestions.valueAddedFromResultsListToInput = true; 321 }, 322 323 preventInputClickBubbling : function (evt){ 324 if(evt.preventDefault){ 325 evt.preventDefault(); 326 } 327 else{ 328 evt.returnValue = false; 329 } 330 if(evt.stopPropagation){ 331 evt.stopPropagation(); 332 } 333 else{ 334 evt.cancelBubble = true; 335 } 336 return false; 337 }, 338 339 preventDefaultForArrowKeys : function (evt){ 340 var keyCode = evt.keyCode; 341 var navigateUp = keyCode == 37 || keyCode == 38; 342 var navigateDown = keyCode == 39 || keyCode == 40; 343 if((!evt.ctrlKey && !evt.metaKey) && ajaxSuggestions.resultsAreVisible && (navigateUp || navigateDown)){ 344 if(evt.preventDefault){ 345 evt.preventDefault(); 346 } 347 else{ 348 evt.returnValue = false; 349 } 350 if(evt.stopPropagation){ 351 evt.stopPropagation(); 352 } 353 else{ 354 evt.cancelBubble = true; 355 } 356 return false; 357 } 358 }, 359 360 getCoordinates : function (){ 361 var elm = this.currentElm; 362 var offsetLeft = 0; 363 var offsetTop = 0; 364 while(elm.offsetParent){ 365 offsetLeft += elm.offsetLeft; 366 offsetTop += elm.offsetTop; 367 if(elm.scrollTop > 0){ 368 offsetTop -= elm.scrollTop; 369 } 370 elm = elm.offsetParent; 371 } 372 return [offsetLeft, offsetTop]; 373 }, 374 375 closeSession : function (){ 376 delete ajaxSuggestions; 377 ajaxSuggestions = null; 378 }, 379 380 elmByClass : function (className, tag){ 381 return ajaxSuggestions.getElementsByClassName.call(this, className, tag); 382 }, 383 384 getElementsByClassName : function (className, tag){ 385 var elms = ((!tag || tag == "*") && this.all)? this.all : this.getElementsByTagName(tag || "*"); 386 var returnElms = []; 387 var className = className.replace(/\-/g, "\\-"); 388 var regExp = new RegExp("(^|\\s)" + className + "(\\s|$)"); 389 var elm; 390 for(var i=0; i<elms.length; i++){ 391 elm = elms[i]; 392 if(regExp.test(elm.className)){ 393 returnElms.push(elm); 394 } 395 } 396 return (returnElms); 397 }, 398 399 addEvent : function (elm, evt, func){ 400 if(elm){ 401 if(elm.addEventListener){ 402 elm.addEventListener(evt, func, false); 403 } 404 else if(window.attachEvent){ 405 elm.attachEvent(("on" + evt), func) 406 } 407 } 408 } 409};
410// --- 411ajaxSuggestions.addEvent(window, "load", function(){ajaxSuggestions.init();}); 412ajaxSuggestions.addEvent(window, "unload", function(){ajaxSuggestions.closeSession();}); 413// ---
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.