annotate media/js/jquery-autocomplete/jquery.autocomplete.js @ 133:c515b7401078

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