| 1 | /* | 
|---|
| 2 |  * jQuery Autocomplete plugin 1.1 | 
|---|
| 3 |  * | 
|---|
| 4 |  * Copyright (c) 2009 Jörn Zaefferer | 
|---|
| 5 |  * | 
|---|
| 6 |  * Dual licensed under the MIT and GPL licenses: | 
|---|
| 7 |  *   http://www.opensource.org/licenses/mit-license.php | 
|---|
| 8 |  *   http://www.gnu.org/licenses/gpl.html | 
|---|
| 9 |  * | 
|---|
| 10 |  * Revision: $Id: jquery.autocomplete.js 15 2009-08-22 10:30:27Z joern.zaefferer $ | 
|---|
| 11 |  */ | 
|---|
| 12 |  | 
|---|
| 13 | ;(function($) { | 
|---|
| 14 |       | 
|---|
| 15 | $.fn.extend({ | 
|---|
| 16 |      autocomplete: function(urlOrData, options) { | 
|---|
| 17 |           var isUrl = typeof urlOrData == "string"; | 
|---|
| 18 |           options = $.extend({}, $.Autocompleter.defaults, { | 
|---|
| 19 |                url: isUrl ? urlOrData : null, | 
|---|
| 20 |                data: isUrl ? null : urlOrData, | 
|---|
| 21 |                delay: isUrl ? $.Autocompleter.defaults.delay : 10, | 
|---|
| 22 |                max: options && !options.scroll ? 10 : 150 | 
|---|
| 23 |           }, options); | 
|---|
| 24 |            | 
|---|
| 25 |           // if highlight is set to false, replace it with a do-nothing function | 
|---|
| 26 |           options.highlight = options.highlight || function(value) { return value; }; | 
|---|
| 27 |            | 
|---|
| 28 |           // if the formatMatch option is not specified, then use formatItem for backwards compatibility | 
|---|
| 29 |           options.formatMatch = options.formatMatch || options.formatItem; | 
|---|
| 30 |            | 
|---|
| 31 |           return this.each(function() { | 
|---|
| 32 |                new $.Autocompleter(this, options); | 
|---|
| 33 |           }); | 
|---|
| 34 |      }, | 
|---|
| 35 |      result: function(handler) { | 
|---|
| 36 |           return this.bind("result", handler); | 
|---|
| 37 |      }, | 
|---|
| 38 |      search: function(handler) { | 
|---|
| 39 |           return this.trigger("search", [handler]); | 
|---|
| 40 |      }, | 
|---|
| 41 |      flushCache: function() { | 
|---|
| 42 |           return this.trigger("flushCache"); | 
|---|
| 43 |      }, | 
|---|
| 44 |      setOptions: function(options){ | 
|---|
| 45 |           return this.trigger("setOptions", [options]); | 
|---|
| 46 |      }, | 
|---|
| 47 |      unautocomplete: function() { | 
|---|
| 48 |           return this.trigger("unautocomplete"); | 
|---|
| 49 |      } | 
|---|
| 50 | }); | 
|---|
| 51 |  | 
|---|
| 52 | $.Autocompleter = function(input, options) { | 
|---|
| 53 |  | 
|---|
| 54 |      var KEY = { | 
|---|
| 55 |           UP: 38, | 
|---|
| 56 |           DOWN: 40, | 
|---|
| 57 |           DEL: 46, | 
|---|
| 58 |           TAB: 9, | 
|---|
| 59 |           RETURN: 13, | 
|---|
| 60 |           ESC: 27, | 
|---|
| 61 |           COMMA: 188, | 
|---|
| 62 |           PAGEUP: 33, | 
|---|
| 63 |           PAGEDOWN: 34, | 
|---|
| 64 |           BACKSPACE: 8 | 
|---|
| 65 |      }; | 
|---|
| 66 |  | 
|---|
| 67 |      // Create $ object for input element | 
|---|
| 68 |      var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass); | 
|---|
| 69 |  | 
|---|
| 70 |      var timeout; | 
|---|
| 71 |      var previousValue = ""; | 
|---|
| 72 |      var cache = $.Autocompleter.Cache(options); | 
|---|
| 73 |      var hasFocus = 0; | 
|---|
| 74 |      var lastKeyPressCode; | 
|---|
| 75 |      var config = { | 
|---|
| 76 |           mouseDownOnSelect: false | 
|---|
| 77 |      }; | 
|---|
| 78 |      var select = $.Autocompleter.Select(options, input, selectCurrent, config); | 
|---|
| 79 |       | 
|---|
| 80 |      var blockSubmit; | 
|---|
| 81 |       | 
|---|
| 82 |      // prevent form submit in opera when selecting with return key | 
|---|
| 83 |      $.browser.opera && $(input.form).bind("submit.autocomplete", function() { | 
|---|
| 84 |           if (blockSubmit) { | 
|---|
| 85 |                blockSubmit = false; | 
|---|
| 86 |                return false; | 
|---|
| 87 |           } | 
|---|
| 88 |      }); | 
|---|
| 89 |       | 
|---|
| 90 |      // only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all | 
|---|
| 91 |      $input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) { | 
|---|
| 92 |           // a keypress means the input has focus | 
|---|
| 93 |           // avoids issue where input had focus before the autocomplete was applied | 
|---|
| 94 |           hasFocus = 1; | 
|---|
| 95 |           // track last key pressed | 
|---|
| 96 |           lastKeyPressCode = event.keyCode; | 
|---|
| 97 |           switch(event.keyCode) { | 
|---|
| 98 |            | 
|---|
| 99 |                case KEY.UP: | 
|---|
| 100 |                     event.preventDefault(); | 
|---|
| 101 |                     if ( select.visible() ) { | 
|---|
| 102 |                          select.prev(); | 
|---|
| 103 |                     } else { | 
|---|
| 104 |                          onChange(0, true); | 
|---|
| 105 |                     } | 
|---|
| 106 |                     break; | 
|---|
| 107 |                      | 
|---|
| 108 |                case KEY.DOWN: | 
|---|
| 109 |                     event.preventDefault(); | 
|---|
| 110 |                     if ( select.visible() ) { | 
|---|
| 111 |                          select.next(); | 
|---|
| 112 |                     } else { | 
|---|
| 113 |                          onChange(0, true); | 
|---|
| 114 |                     } | 
|---|
| 115 |                     break; | 
|---|
| 116 |                      | 
|---|
| 117 |                case KEY.PAGEUP: | 
|---|
| 118 |                     event.preventDefault(); | 
|---|
| 119 |                     if ( select.visible() ) { | 
|---|
| 120 |                          select.pageUp(); | 
|---|
| 121 |                     } else { | 
|---|
| 122 |                          onChange(0, true); | 
|---|
| 123 |                     } | 
|---|
| 124 |                     break; | 
|---|
| 125 |                      | 
|---|
| 126 |                case KEY.PAGEDOWN: | 
|---|
| 127 |                     event.preventDefault(); | 
|---|
| 128 |                     if ( select.visible() ) { | 
|---|
| 129 |                          select.pageDown(); | 
|---|
| 130 |                     } else { | 
|---|
| 131 |                          onChange(0, true); | 
|---|
| 132 |                     } | 
|---|
| 133 |                     break; | 
|---|
| 134 |                 | 
|---|
| 135 |                // matches also semicolon | 
|---|
| 136 |                case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA: | 
|---|
| 137 |                case KEY.TAB: | 
|---|
| 138 |                case KEY.RETURN: | 
|---|
| 139 |                     if( selectCurrent() ) { | 
|---|
| 140 |                          // stop default to prevent a form submit, Opera needs special handling | 
|---|
| 141 |                          event.preventDefault(); | 
|---|
| 142 |                          blockSubmit = true; | 
|---|
| 143 |                          return false; | 
|---|
| 144 |                     } | 
|---|
| 145 |                     break; | 
|---|
| 146 |                      | 
|---|
| 147 |                case KEY.ESC: | 
|---|
| 148 |                     select.hide(); | 
|---|
| 149 |                     break; | 
|---|
| 150 |                      | 
|---|
| 151 |                default: | 
|---|
| 152 |                     clearTimeout(timeout); | 
|---|
| 153 |                     timeout = setTimeout(onChange, options.delay); | 
|---|
| 154 |                     break; | 
|---|
| 155 |           } | 
|---|
| 156 |      }).focus(function(){ | 
|---|
| 157 |           // track whether the field has focus, we shouldn't process any | 
|---|
| 158 |           // results if the field no longer has focus | 
|---|
| 159 |           hasFocus++; | 
|---|
| 160 |      }).blur(function() { | 
|---|
| 161 |           hasFocus = 0; | 
|---|
| 162 |           if (!config.mouseDownOnSelect) { | 
|---|
| 163 |                hideResults(); | 
|---|
| 164 |           } | 
|---|
| 165 |      }).click(function() { | 
|---|
| 166 |           // show select when clicking in a focused field | 
|---|
| 167 |           if ( hasFocus++ > 1 && !select.visible() ) { | 
|---|
| 168 |                onChange(0, true); | 
|---|
| 169 |           } | 
|---|
| 170 |      }).bind("search", function() { | 
|---|
| 171 |           // TODO why not just specifying both arguments? | 
|---|
| 172 |           var fn = (arguments.length > 1) ? arguments[1] : null; | 
|---|
| 173 |           function findValueCallback(q, data) { | 
|---|
| 174 |                var result; | 
|---|
| 175 |                if( data && data.length ) { | 
|---|
| 176 |                     for (var i=0; i < data.length; i++) { | 
|---|
| 177 |                          if( data[i].result.toLowerCase() == q.toLowerCase() ) { | 
|---|
| 178 |                               result = data[i]; | 
|---|
| 179 |                               break; | 
|---|
| 180 |                          } | 
|---|
| 181 |                     } | 
|---|
| 182 |                } | 
|---|
| 183 |                if( typeof fn == "function" ) fn(result); | 
|---|
| 184 |                else $input.trigger("result", result && [result.data, result.value]); | 
|---|
| 185 |           } | 
|---|
| 186 |           $.each(trimWords($input.val()), function(i, value) { | 
|---|
| 187 |                request(value, findValueCallback, findValueCallback); | 
|---|
| 188 |           }); | 
|---|
| 189 |      }).bind("flushCache", function() { | 
|---|
| 190 |           cache.flush(); | 
|---|
| 191 |      }).bind("setOptions", function() { | 
|---|
| 192 |           $.extend(options, arguments[1]); | 
|---|
| 193 |           // if we've updated the data, repopulate | 
|---|
| 194 |           if ( "data" in arguments[1] ) | 
|---|
| 195 |                cache.populate(); | 
|---|
| 196 |      }).bind("unautocomplete", function() { | 
|---|
| 197 |           select.unbind(); | 
|---|
| 198 |           $input.unbind(); | 
|---|
| 199 |           $(input.form).unbind(".autocomplete"); | 
|---|
| 200 |      }); | 
|---|
| 201 |       | 
|---|
| 202 |       | 
|---|
| 203 |      function selectCurrent() { | 
|---|
| 204 |           var selected = select.selected(); | 
|---|
| 205 |           if( !selected ) | 
|---|
| 206 |                return false; | 
|---|
| 207 |            | 
|---|
| 208 |           var v = selected.result; | 
|---|
| 209 |           previousValue = v; | 
|---|
| 210 |            | 
|---|
| 211 |           if ( options.multiple ) { | 
|---|
| 212 |                var words = trimWords($input.val()); | 
|---|
| 213 |                if ( words.length > 1 ) { | 
|---|
| 214 |                     var seperator = options.multipleSeparator.length; | 
|---|
| 215 |                     var cursorAt = $(input).selection().start; | 
|---|
| 216 |                     var wordAt, progress = 0; | 
|---|
| 217 |                     $.each(words, function(i, word) { | 
|---|
| 218 |                          progress += word.length; | 
|---|
| 219 |                          if (cursorAt <= progress) { | 
|---|
| 220 |                               wordAt = i; | 
|---|
| 221 |                               return false; | 
|---|
| 222 |                          } | 
|---|
| 223 |                          progress += seperator; | 
|---|
| 224 |                     }); | 
|---|
| 225 |                     words[wordAt] = v; | 
|---|
| 226 |                     // TODO this should set the cursor to the right position, but it gets overriden somewhere | 
|---|
| 227 |                     //$.Autocompleter.Selection(input, progress + seperator, progress + seperator); | 
|---|
| 228 |                     v = words.join( options.multipleSeparator ); | 
|---|
| 229 |                } | 
|---|
| 230 |                v += options.multipleSeparator; | 
|---|
| 231 |           } | 
|---|
| 232 |            | 
|---|
| 233 |           $input.val(v); | 
|---|
| 234 |           hideResultsNow(); | 
|---|
| 235 |           $input.trigger("result", [selected.data, selected.value]); | 
|---|
| 236 |           return true; | 
|---|
| 237 |      } | 
|---|
| 238 |       | 
|---|
| 239 |      function onChange(crap, skipPrevCheck) { | 
|---|
| 240 |           if( lastKeyPressCode == KEY.DEL ) { | 
|---|
| 241 |                select.hide(); | 
|---|
| 242 |                return; | 
|---|
| 243 |           } | 
|---|
| 244 |            | 
|---|
| 245 |           var currentValue = $input.val(); | 
|---|
| 246 |            | 
|---|
| 247 |           if ( !skipPrevCheck && currentValue == previousValue ) | 
|---|
| 248 |                return; | 
|---|
| 249 |            | 
|---|
| 250 |           previousValue = currentValue; | 
|---|
| 251 |            | 
|---|
| 252 |           currentValue = lastWord(currentValue); | 
|---|
| 253 |           if ( currentValue.length >= options.minChars) { | 
|---|
| 254 |                $input.addClass(options.loadingClass); | 
|---|
| 255 |                if (!options.matchCase) | 
|---|
| 256 |                     currentValue = currentValue.toLowerCase(); | 
|---|
| 257 |                request(currentValue, receiveData, hideResultsNow); | 
|---|
| 258 |           } else { | 
|---|
| 259 |                stopLoading(); | 
|---|
| 260 |                select.hide(); | 
|---|
| 261 |           } | 
|---|
| 262 |      }; | 
|---|
| 263 |       | 
|---|
| 264 |      function trimWords(value) { | 
|---|
| 265 |           if (!value) | 
|---|
| 266 |                return [""]; | 
|---|
| 267 |           if (!options.multiple) | 
|---|
| 268 |                return [$.trim(value)]; | 
|---|
| 269 |           return $.map(value.split(options.multipleSeparator), function(word) { | 
|---|
| 270 |                return $.trim(value).length ? $.trim(word) : null; | 
|---|
| 271 |           }); | 
|---|
| 272 |      } | 
|---|
| 273 |       | 
|---|
| 274 |      function lastWord(value) { | 
|---|
| 275 |           if ( !options.multiple ) | 
|---|
| 276 |                return value; | 
|---|
| 277 |           var words = trimWords(value); | 
|---|
| 278 |           if (words.length == 1)  | 
|---|
| 279 |                return words[0]; | 
|---|
| 280 |           var cursorAt = $(input).selection().start; | 
|---|
| 281 |           if (cursorAt == value.length) { | 
|---|
| 282 |                words = trimWords(value) | 
|---|
| 283 |           } else { | 
|---|
| 284 |                words = trimWords(value.replace(value.substring(cursorAt), "")); | 
|---|
| 285 |           } | 
|---|
| 286 |           return words[words.length - 1]; | 
|---|
| 287 |      } | 
|---|
| 288 |       | 
|---|
| 289 |      // fills in the input box w/the first match (assumed to be the best match) | 
|---|
| 290 |      // q: the term entered | 
|---|
| 291 |      // sValue: the first matching result | 
|---|
| 292 |      function autoFill(q, sValue){ | 
|---|
| 293 |           // autofill in the complete box w/the first match as long as the user hasn't entered in more data | 
|---|
| 294 |           // if the last user key pressed was backspace, don't autofill | 
|---|
| 295 |           if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) { | 
|---|
| 296 |                // fill in the value (keep the case the user has typed) | 
|---|
| 297 |                $input.val($input.val() + sValue.substring(lastWord(previousValue).length)); | 
|---|
| 298 |                // select the portion of the value not typed by the user (so the next character will erase) | 
|---|
| 299 |                $(input).selection(previousValue.length, previousValue.length + sValue.length); | 
|---|
| 300 |           } | 
|---|
| 301 |      }; | 
|---|
| 302 |  | 
|---|
| 303 |      function hideResults() { | 
|---|
| 304 |           clearTimeout(timeout); | 
|---|
| 305 |           timeout = setTimeout(hideResultsNow, 200); | 
|---|
| 306 |      }; | 
|---|
| 307 |  | 
|---|
| 308 |      function hideResultsNow() { | 
|---|
| 309 |           var wasVisible = select.visible(); | 
|---|
| 310 |           select.hide(); | 
|---|
| 311 |           clearTimeout(timeout); | 
|---|
| 312 |           stopLoading(); | 
|---|
| 313 |           if (options.mustMatch) { | 
|---|
| 314 |                // call search and run callback | 
|---|
| 315 |                $input.search( | 
|---|
| 316 |                     function (result){ | 
|---|
| 317 |                          // if no value found, clear the input box | 
|---|
| 318 |                          if( !result ) { | 
|---|
| 319 |                               if (options.multiple) { | 
|---|
| 320 |                                    var words = trimWords($input.val()).slice(0, -1); | 
|---|
| 321 |                                    $input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") ); | 
|---|
| 322 |                               } | 
|---|
| 323 |                               else { | 
|---|
| 324 |                                    $input.val( "" ); | 
|---|
| 325 |                                    $input.trigger("result", null); | 
|---|
| 326 |                               } | 
|---|
| 327 |                          } | 
|---|
| 328 |                     } | 
|---|
| 329 |                ); | 
|---|
| 330 |           } | 
|---|
| 331 |      }; | 
|---|
| 332 |  | 
|---|
| 333 |      function receiveData(q, data) { | 
|---|
| 334 |           if ( data && data.length && hasFocus ) { | 
|---|
| 335 |                stopLoading(); | 
|---|
| 336 |                select.display(data, q); | 
|---|
| 337 |                autoFill(q, data[0].value); | 
|---|
| 338 |                select.show(); | 
|---|
| 339 |           } else { | 
|---|
| 340 |                hideResultsNow(); | 
|---|
| 341 |           } | 
|---|
| 342 |      }; | 
|---|
| 343 |  | 
|---|
| 344 |      function request(term, success, failure) { | 
|---|
| 345 |           if (!options.matchCase) | 
|---|
| 346 |                term = term.toLowerCase(); | 
|---|
| 347 |           var data = cache.load(term); | 
|---|
| 348 |           // recieve the cached data | 
|---|
| 349 |           if (data && data.length) { | 
|---|
| 350 |                success(term, data); | 
|---|
| 351 |           // if an AJAX url has been supplied, try loading the data now | 
|---|
| 352 |           } else if( (typeof options.url == "string") && (options.url.length > 0) ){ | 
|---|
| 353 |                 | 
|---|
| 354 |                var extraParams = { | 
|---|
| 355 |                     timestamp: +new Date() | 
|---|
| 356 |                }; | 
|---|
| 357 |                $.each(options.extraParams, function(key, param) { | 
|---|
| 358 |                     extraParams[key] = typeof param == "function" ? param() : param; | 
|---|
| 359 |                }); | 
|---|
| 360 |                 | 
|---|
| 361 |                $.ajax({ | 
|---|
| 362 |                     // try to leverage ajaxQueue plugin to abort previous requests | 
|---|
| 363 |                     mode: "abort", | 
|---|
| 364 |                     // limit abortion to this input | 
|---|
| 365 |                     port: "autocomplete" + input.name, | 
|---|
| 366 |                     dataType: options.dataType, | 
|---|
| 367 |                     url: options.url, | 
|---|
| 368 |                     data: $.extend({ | 
|---|
| 369 |                          q: lastWord(term), | 
|---|
| 370 |                          limit: options.max | 
|---|
| 371 |                     }, extraParams), | 
|---|
| 372 |                     success: function(data) { | 
|---|
| 373 |                          var parsed = options.parse && options.parse(data) || parse(data); | 
|---|
| 374 |                          cache.add(term, parsed); | 
|---|
| 375 |                          success(term, parsed); | 
|---|
| 376 |                     } | 
|---|
| 377 |                }); | 
|---|
| 378 |           } else { | 
|---|
| 379 |                // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match | 
|---|
| 380 |                select.emptyList(); | 
|---|
| 381 |                failure(term); | 
|---|
| 382 |           } | 
|---|
| 383 |      }; | 
|---|
| 384 |       | 
|---|
| 385 |      function parse(data) { | 
|---|
| 386 |           var parsed = []; | 
|---|
| 387 |           var rows = data.split("\n"); | 
|---|
| 388 |           for (var i=0; i < rows.length; i++) { | 
|---|
| 389 |                var row = $.trim(rows[i]); | 
|---|
| 390 |                if (row) { | 
|---|
| 391 |                     row = row.split("|"); | 
|---|
| 392 |                     parsed[parsed.length] = { | 
|---|
| 393 |                          data: row, | 
|---|
| 394 |                          value: row[0], | 
|---|
| 395 |                          result: options.formatResult && options.formatResult(row, row[0]) || row[0] | 
|---|
| 396 |                     }; | 
|---|
| 397 |                } | 
|---|
| 398 |           } | 
|---|
| 399 |           return parsed; | 
|---|
| 400 |      }; | 
|---|
| 401 |  | 
|---|
| 402 |      function stopLoading() { | 
|---|
| 403 |           $input.removeClass(options.loadingClass); | 
|---|
| 404 |      }; | 
|---|
| 405 |  | 
|---|
| 406 | }; | 
|---|
| 407 |  | 
|---|
| 408 | $.Autocompleter.defaults = { | 
|---|
| 409 |      inputClass: "ac_input", | 
|---|
| 410 |      resultsClass: "ac_results", | 
|---|
| 411 |      loadingClass: "ac_loading", | 
|---|
| 412 |      minChars: 1, | 
|---|
| 413 |      delay: 400, | 
|---|
| 414 |      matchCase: false, | 
|---|
| 415 |      matchSubset: true, | 
|---|
| 416 |      matchContains: false, | 
|---|
| 417 |      cacheLength: 10, | 
|---|
| 418 |      max: 100, | 
|---|
| 419 |      mustMatch: false, | 
|---|
| 420 |      extraParams: {}, | 
|---|
| 421 |      selectFirst: true, | 
|---|
| 422 |      formatItem: function(row) { return row[0]; }, | 
|---|
| 423 |      formatMatch: null, | 
|---|
| 424 |      autoFill: false, | 
|---|
| 425 |      width: 0, | 
|---|
| 426 |      multiple: false, | 
|---|
| 427 |      multipleSeparator: ", ", | 
|---|
| 428 |      highlight: function(value, term) { | 
|---|
| 429 |           return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>"); | 
|---|
| 430 |      }, | 
|---|
| 431 |     scroll: true, | 
|---|
| 432 |     scrollHeight: 180 | 
|---|
| 433 | }; | 
|---|
| 434 |  | 
|---|
| 435 | $.Autocompleter.Cache = function(options) { | 
|---|
| 436 |  | 
|---|
| 437 |      var data = {}; | 
|---|
| 438 |      var length = 0; | 
|---|
| 439 |       | 
|---|
| 440 |      function matchSubset(s, sub) { | 
|---|
| 441 |           if (!options.matchCase)  | 
|---|
| 442 |                s = s.toLowerCase(); | 
|---|
| 443 |           var i = s.indexOf(sub); | 
|---|
| 444 |           if (options.matchContains == "word"){ | 
|---|
| 445 |                i = s.toLowerCase().search("\\b" + sub.toLowerCase()); | 
|---|
| 446 |           } | 
|---|
| 447 |           if (i == -1) return false; | 
|---|
| 448 |           return i == 0 || options.matchContains; | 
|---|
| 449 |      }; | 
|---|
| 450 |       | 
|---|
| 451 |      function add(q, value) { | 
|---|
| 452 |           if (length > options.cacheLength){ | 
|---|
| 453 |                flush(); | 
|---|
| 454 |           } | 
|---|
| 455 |           if (!data[q]){  | 
|---|
| 456 |                length++; | 
|---|
| 457 |           } | 
|---|
| 458 |           data[q] = value; | 
|---|
| 459 |      } | 
|---|
| 460 |       | 
|---|
| 461 |      function populate(){ | 
|---|
| 462 |           if( !options.data ) return false; | 
|---|
| 463 |           // track the matches | 
|---|
| 464 |           var stMatchSets = {}, | 
|---|
| 465 |                nullData = 0; | 
|---|
| 466 |  | 
|---|
| 467 |           // no url was specified, we need to adjust the cache length to make sure it fits the local data store | 
|---|
| 468 |           if( !options.url ) options.cacheLength = 1; | 
|---|
| 469 |            | 
|---|
| 470 |           // track all options for minChars = 0 | 
|---|
| 471 |           stMatchSets[""] = []; | 
|---|
| 472 |            | 
|---|
| 473 |           // loop through the array and create a lookup structure | 
|---|
| 474 |           for ( var i = 0, ol = options.data.length; i < ol; i++ ) { | 
|---|
| 475 |                var rawValue = options.data[i]; | 
|---|
| 476 |                // if rawValue is a string, make an array otherwise just reference the array | 
|---|
| 477 |                rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue; | 
|---|
| 478 |                 | 
|---|
| 479 |                var value = options.formatMatch(rawValue, i+1, options.data.length); | 
|---|
| 480 |                if ( value === false ) | 
|---|
| 481 |                     continue; | 
|---|
| 482 |                      | 
|---|
| 483 |                var firstChar = value.charAt(0).toLowerCase(); | 
|---|
| 484 |                // if no lookup array for this character exists, look it up now | 
|---|
| 485 |                if( !stMatchSets[firstChar] )  | 
|---|
| 486 |                     stMatchSets[firstChar] = []; | 
|---|
| 487 |  | 
|---|
| 488 |                // if the match is a string | 
|---|
| 489 |                var row = { | 
|---|
| 490 |                     value: value, | 
|---|
| 491 |                     data: rawValue, | 
|---|
| 492 |                     result: options.formatResult && options.formatResult(rawValue) || value | 
|---|
| 493 |                }; | 
|---|
| 494 |                 | 
|---|
| 495 |                // push the current match into the set list | 
|---|
| 496 |                stMatchSets[firstChar].push(row); | 
|---|
| 497 |  | 
|---|
| 498 |                // keep track of minChars zero items | 
|---|
| 499 |                if ( nullData++ < options.max ) { | 
|---|
| 500 |                     stMatchSets[""].push(row); | 
|---|
| 501 |                } | 
|---|
| 502 |           }; | 
|---|
| 503 |  | 
|---|
| 504 |           // add the data items to the cache | 
|---|
| 505 |           $.each(stMatchSets, function(i, value) { | 
|---|
| 506 |                // increase the cache size | 
|---|
| 507 |                options.cacheLength++; | 
|---|
| 508 |                // add to the cache | 
|---|
| 509 |                add(i, value); | 
|---|
| 510 |           }); | 
|---|
| 511 |      } | 
|---|
| 512 |       | 
|---|
| 513 |      // populate any existing data | 
|---|
| 514 |      setTimeout(populate, 25); | 
|---|
| 515 |       | 
|---|
| 516 |      function flush(){ | 
|---|
| 517 |           data = {}; | 
|---|
| 518 |           length = 0; | 
|---|
| 519 |      } | 
|---|
| 520 |       | 
|---|
| 521 |      return { | 
|---|
| 522 |           flush: flush, | 
|---|
| 523 |           add: add, | 
|---|
| 524 |           populate: populate, | 
|---|
| 525 |           load: function(q) { | 
|---|
| 526 |                if (!options.cacheLength || !length) | 
|---|
| 527 |                     return null; | 
|---|
| 528 |                /*  | 
|---|
| 529 |                 * if dealing w/local data and matchContains than we must make sure | 
|---|
| 530 |                 * to loop through all the data collections looking for matches | 
|---|
| 531 |                 */ | 
|---|
| 532 |                if( !options.url && options.matchContains ){ | 
|---|
| 533 |                     // track all matches | 
|---|
| 534 |                     var csub = []; | 
|---|
| 535 |                     // loop through all the data grids for matches | 
|---|
| 536 |                     for( var k in data ){ | 
|---|
| 537 |                          // don't search through the stMatchSets[""] (minChars: 0) cache | 
|---|
| 538 |                          // this prevents duplicates | 
|---|
| 539 |                          if( k.length > 0 ){ | 
|---|
| 540 |                               var c = data[k]; | 
|---|
| 541 |                               $.each(c, function(i, x) { | 
|---|
| 542 |                                    // if we've got a match, add it to the array | 
|---|
| 543 |                                    if (matchSubset(x.value, q)) { | 
|---|
| 544 |                                         csub.push(x); | 
|---|
| 545 |                                    } | 
|---|
| 546 |                               }); | 
|---|
| 547 |                          } | 
|---|
| 548 |                     }                    | 
|---|
| 549 |                     return csub; | 
|---|
| 550 |                } else  | 
|---|
| 551 |                // if the exact item exists, use it | 
|---|
| 552 |                if (data[q]){ | 
|---|
| 553 |                     return data[q]; | 
|---|
| 554 |                } else | 
|---|
| 555 |                if (options.matchSubset) { | 
|---|
| 556 |                     for (var i = q.length - 1; i >= options.minChars; i--) { | 
|---|
| 557 |                          var c = data[q.substr(0, i)]; | 
|---|
| 558 |                          if (c) { | 
|---|
| 559 |                               var csub = []; | 
|---|
| 560 |                               $.each(c, function(i, x) { | 
|---|
| 561 |                                    if (matchSubset(x.value, q)) { | 
|---|
| 562 |                                         csub[csub.length] = x; | 
|---|
| 563 |                                    } | 
|---|
| 564 |                               }); | 
|---|
| 565 |                               return csub; | 
|---|
| 566 |                          } | 
|---|
| 567 |                     } | 
|---|
| 568 |                } | 
|---|
| 569 |                return null; | 
|---|
| 570 |           } | 
|---|
| 571 |      }; | 
|---|
| 572 | }; | 
|---|
| 573 |  | 
|---|
| 574 | $.Autocompleter.Select = function (options, input, select, config) { | 
|---|
| 575 |      var CLASSES = { | 
|---|
| 576 |           ACTIVE: "ac_over" | 
|---|
| 577 |      }; | 
|---|
| 578 |       | 
|---|
| 579 |      var listItems, | 
|---|
| 580 |           active = -1, | 
|---|
| 581 |           data, | 
|---|
| 582 |           term = "", | 
|---|
| 583 |           needsInit = true, | 
|---|
| 584 |           element, | 
|---|
| 585 |           list; | 
|---|
| 586 |       | 
|---|
| 587 |      // Create results | 
|---|
| 588 |      function init() { | 
|---|
| 589 |           if (!needsInit) | 
|---|
| 590 |                return; | 
|---|
| 591 |           element = $("<div/>") | 
|---|
| 592 |           .hide() | 
|---|
| 593 |           .addClass(options.resultsClass) | 
|---|
| 594 |           .css("position", "absolute") | 
|---|
| 595 |           .appendTo(document.body); | 
|---|
| 596 |       | 
|---|
| 597 |           list = $("<ul/>").appendTo(element).mouseover( function(event) { | 
|---|
| 598 |                if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') { | 
|---|
| 599 |                  active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event)); | 
|---|
| 600 |                    $(target(event)).addClass(CLASSES.ACTIVE);             | 
|---|
| 601 |              } | 
|---|
| 602 |           }).click(function(event) { | 
|---|
| 603 |                $(target(event)).addClass(CLASSES.ACTIVE); | 
|---|
| 604 |                select(); | 
|---|
| 605 |                // TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus | 
|---|
| 606 |                input.focus(); | 
|---|
| 607 |                return false; | 
|---|
| 608 |           }).mousedown(function() { | 
|---|
| 609 |                config.mouseDownOnSelect = true; | 
|---|
| 610 |           }).mouseup(function() { | 
|---|
| 611 |                config.mouseDownOnSelect = false; | 
|---|
| 612 |           }); | 
|---|
| 613 |            | 
|---|
| 614 |           if( options.width > 0 ) | 
|---|
| 615 |                element.css("width", options.width); | 
|---|
| 616 |                 | 
|---|
| 617 |           needsInit = false; | 
|---|
| 618 |      }  | 
|---|
| 619 |       | 
|---|
| 620 |      function target(event) { | 
|---|
| 621 |           var element = event.target; | 
|---|
| 622 |           while(element && element.tagName != "LI") | 
|---|
| 623 |                element = element.parentNode; | 
|---|
| 624 |           // more fun with IE, sometimes event.target is empty, just ignore it then | 
|---|
| 625 |           if(!element) | 
|---|
| 626 |                return []; | 
|---|
| 627 |           return element; | 
|---|
| 628 |      } | 
|---|
| 629 |  | 
|---|
| 630 |      function moveSelect(step) { | 
|---|
| 631 |           listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE); | 
|---|
| 632 |           movePosition(step); | 
|---|
| 633 |         var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE); | 
|---|
| 634 |         if(options.scroll) { | 
|---|
| 635 |             var offset = 0; | 
|---|
| 636 |             listItems.slice(0, active).each(function() { | 
|---|
| 637 |                     offset += this.offsetHeight; | 
|---|
| 638 |                }); | 
|---|
| 639 |             if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) { | 
|---|
| 640 |                 list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight()); | 
|---|
| 641 |             } else if(offset < list.scrollTop()) { | 
|---|
| 642 |                 list.scrollTop(offset); | 
|---|
| 643 |             } | 
|---|
| 644 |         } | 
|---|
| 645 |      }; | 
|---|
| 646 |       | 
|---|
| 647 |      function movePosition(step) { | 
|---|
| 648 |           active += step; | 
|---|
| 649 |           if (active < 0) { | 
|---|
| 650 |                active = listItems.size() - 1; | 
|---|
| 651 |           } else if (active >= listItems.size()) { | 
|---|
| 652 |                active = 0; | 
|---|
| 653 |           } | 
|---|
| 654 |      } | 
|---|
| 655 |       | 
|---|
| 656 |      function limitNumberOfItems(available) { | 
|---|
| 657 |           return options.max && options.max < available | 
|---|
| 658 |                ? options.max | 
|---|
| 659 |                : available; | 
|---|
| 660 |      } | 
|---|
| 661 |       | 
|---|
| 662 |      function fillList() { | 
|---|
| 663 |           list.empty(); | 
|---|
| 664 |           var max = limitNumberOfItems(data.length); | 
|---|
| 665 |           for (var i=0; i < max; i++) { | 
|---|
| 666 |                if (!data[i]) | 
|---|
| 667 |                     continue; | 
|---|
| 668 |                var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term); | 
|---|
| 669 |                if ( formatted === false ) | 
|---|
| 670 |                     continue; | 
|---|
| 671 |                var li = $("<li/>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0]; | 
|---|
| 672 |                $.data(li, "ac_data", data[i]); | 
|---|
| 673 |           } | 
|---|
| 674 |           listItems = list.find("li"); | 
|---|
| 675 |           if ( options.selectFirst ) { | 
|---|
| 676 |                listItems.slice(0, 1).addClass(CLASSES.ACTIVE); | 
|---|
| 677 |                active = 0; | 
|---|
| 678 |           } | 
|---|
| 679 |           // apply bgiframe if available | 
|---|
| 680 |           if ( $.fn.bgiframe ) | 
|---|
| 681 |                list.bgiframe(); | 
|---|
| 682 |      } | 
|---|
| 683 |       | 
|---|
| 684 |      return { | 
|---|
| 685 |           display: function(d, q) { | 
|---|
| 686 |                init(); | 
|---|
| 687 |                data = d; | 
|---|
| 688 |                term = q; | 
|---|
| 689 |                fillList(); | 
|---|
| 690 |           }, | 
|---|
| 691 |           next: function() { | 
|---|
| 692 |                moveSelect(1); | 
|---|
| 693 |           }, | 
|---|
| 694 |           prev: function() { | 
|---|
| 695 |                moveSelect(-1); | 
|---|
| 696 |           }, | 
|---|
| 697 |           pageUp: function() { | 
|---|
| 698 |                if (active != 0 && active - 8 < 0) { | 
|---|
| 699 |                     moveSelect( -active ); | 
|---|
| 700 |                } else { | 
|---|
| 701 |                     moveSelect(-8); | 
|---|
| 702 |                } | 
|---|
| 703 |           }, | 
|---|
| 704 |           pageDown: function() { | 
|---|
| 705 |                if (active != listItems.size() - 1 && active + 8 > listItems.size()) { | 
|---|
| 706 |                     moveSelect( listItems.size() - 1 - active ); | 
|---|
| 707 |                } else { | 
|---|
| 708 |                     moveSelect(8); | 
|---|
| 709 |                } | 
|---|
| 710 |           }, | 
|---|
| 711 |           hide: function() { | 
|---|
| 712 |                element && element.hide(); | 
|---|
| 713 |                listItems && listItems.removeClass(CLASSES.ACTIVE); | 
|---|
| 714 |                active = -1; | 
|---|
| 715 |           }, | 
|---|
| 716 |           visible : function() { | 
|---|
| 717 |                return element && element.is(":visible"); | 
|---|
| 718 |           }, | 
|---|
| 719 |           current: function() { | 
|---|
| 720 |                return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]); | 
|---|
| 721 |           }, | 
|---|
| 722 |           show: function() { | 
|---|
| 723 |                var offset = $(input).offset(); | 
|---|
| 724 |                element.css({ | 
|---|
| 725 |                     width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(), | 
|---|
| 726 |                     top: offset.top + input.offsetHeight, | 
|---|
| 727 |                     left: offset.left | 
|---|
| 728 |                }).show(); | 
|---|
| 729 |             if(options.scroll) { | 
|---|
| 730 |                 list.scrollTop(0); | 
|---|
| 731 |                 list.css({ | 
|---|
| 732 |                          maxHeight: options.scrollHeight, | 
|---|
| 733 |                          overflow: 'auto' | 
|---|
| 734 |                     }); | 
|---|
| 735 |                      | 
|---|
| 736 |                 if($.browser.msie && typeof document.body.style.maxHeight === "undefined") { | 
|---|
| 737 |                          var listHeight = 0; | 
|---|
| 738 |                          listItems.each(function() { | 
|---|
| 739 |                               listHeight += this.offsetHeight; | 
|---|
| 740 |                          }); | 
|---|
| 741 |                          var scrollbarsVisible = listHeight > options.scrollHeight; | 
|---|
| 742 |                     list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight ); | 
|---|
| 743 |                          if (!scrollbarsVisible) { | 
|---|
| 744 |                               // IE doesn't recalculate width when scrollbar disappears | 
|---|
| 745 |                               listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) ); | 
|---|
| 746 |                          } | 
|---|
| 747 |                 } | 
|---|
| 748 |                  | 
|---|
| 749 |             } | 
|---|
| 750 |           }, | 
|---|
| 751 |           selected: function() { | 
|---|
| 752 |                var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE); | 
|---|
| 753 |                return selected && selected.length && $.data(selected[0], "ac_data"); | 
|---|
| 754 |           }, | 
|---|
| 755 |           emptyList: function (){ | 
|---|
| 756 |                list && list.empty(); | 
|---|
| 757 |           }, | 
|---|
| 758 |           unbind: function() { | 
|---|
| 759 |                element && element.remove(); | 
|---|
| 760 |           } | 
|---|
| 761 |      }; | 
|---|
| 762 | }; | 
|---|
| 763 |  | 
|---|
| 764 | $.fn.selection = function(start, end) { | 
|---|
| 765 |      if (start !== undefined) { | 
|---|
| 766 |           return this.each(function() { | 
|---|
| 767 |                if( this.createTextRange ){ | 
|---|
| 768 |                     var selRange = this.createTextRange(); | 
|---|
| 769 |                     if (end === undefined || start == end) { | 
|---|
| 770 |                          selRange.move("character", start); | 
|---|
| 771 |                          selRange.select(); | 
|---|
| 772 |                     } else { | 
|---|
| 773 |                          selRange.collapse(true); | 
|---|
| 774 |                          selRange.moveStart("character", start); | 
|---|
| 775 |                          selRange.moveEnd("character", end); | 
|---|
| 776 |                          selRange.select(); | 
|---|
| 777 |                     } | 
|---|
| 778 |                } else if( this.setSelectionRange ){ | 
|---|
| 779 |                     this.setSelectionRange(start, end); | 
|---|
| 780 |                } else if( this.selectionStart ){ | 
|---|
| 781 |                     this.selectionStart = start; | 
|---|
| 782 |                     this.selectionEnd = end; | 
|---|
| 783 |                } | 
|---|
| 784 |           }); | 
|---|
| 785 |      } | 
|---|
| 786 |      var field = this[0]; | 
|---|
| 787 |      if ( field.createTextRange ) { | 
|---|
| 788 |           var range = document.selection.createRange(), | 
|---|
| 789 |                orig = field.value, | 
|---|
| 790 |                teststring = "<->", | 
|---|
| 791 |                textLength = range.text.length; | 
|---|
| 792 |           range.text = teststring; | 
|---|
| 793 |           var caretAt = field.value.indexOf(teststring); | 
|---|
| 794 |           field.value = orig; | 
|---|
| 795 |           this.selection(caretAt, caretAt + textLength); | 
|---|
| 796 |           return { | 
|---|
| 797 |                start: caretAt, | 
|---|
| 798 |                end: caretAt + textLength | 
|---|
| 799 |           } | 
|---|
| 800 |      } else if( field.selectionStart !== undefined ){ | 
|---|
| 801 |           return { | 
|---|
| 802 |                start: field.selectionStart, | 
|---|
| 803 |                end: field.selectionEnd | 
|---|
| 804 |           } | 
|---|
| 805 |      } | 
|---|
| 806 | }; | 
|---|
| 807 |  | 
|---|
| 808 | })(jQuery); | 
|---|