Streber PM PHP Cross Reference Groupware Applications

Source: /js/jquery.autocomplete.1.0.2.js - 759 lines - 25233 bytes - Summary - Text - Print

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

title

Description

title

Description

title

Description

title

title

Body