<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
 <title>completer demo</title>
 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
 <script src="/lib/prototype.js" type="text/javascript"></script>
 <style type="text/css">
 body {
  font: 11px Lucida Grande, Verdana, Arial, Helvetica, sans serif;
 }
 </style>
</head>
<body> 
 <br/>
 <div id="content">
  <form>
   input "a" for test:
   <input id="message" name="message" size="30" type="text" />
   <input type="submit" value="submit"/>
  </form>
  <script type="text/javascript">
  //<![CDATA[
  var contacts = ["aa", "aaa", "aaaa", "abb", "abbb", "ac", "acc", "accc", "aac", "aaac", "bc", "ad", "aad", "aaad", "aaaad", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "中文字体", "测试~!@#$%^&*()/?\"\\';", "北京奥运会", "姚明&刘翔"];
  
  AutoFieldFromLocal = Class.create({
   initialize: function(element, items){
    this.element = $(element);
    this.value = this.element.value;
    this.items = items;
    this.timer = null;
    this.observer = null;
    this.counter = 0;
    this.index = 0;
    this.listElements = [];
    this.listsObserved = false;
    this.maskShowed = false;
    this.divId = "auto_complete_for_" + this.element.id;
    this.listsId = "lists_of_" + this.element.id;
    this.maskId = "mask_" + this.element.id;
    this.element.setAttribute('autocomplete', 'off');
    this.insertAfter();
    this.observerStart();
   },
   insertAfter: function(){
    new Insertion.After(this.element, 
     '<div id="' + this.divId + '" style="position:absolute;background-color:#fff;border:1px solid #888;margin:0;padding:0;display:none;height:200px;opacity:0.90;overflow:auto;overflow-x:hidden;z-index:1;filter:alpha(opacity=90);">' + 
     '<ul id="' + this.listsId + '" style="margin:0;padding:0;list-style-type: none;width:100%;"></ul></div>' +
     '<div id="' + this.maskId + '" style="position:absolute;background-color:blue;border:1px solid #888;margin:0;padding:0;display:none;height:0;opacity:0;z-index:2;filter:alpha(opacity=0);"/>'
     );
    this.completerDiv = $(this.divId);
    this.list = $(this.listsId);
    this.mask = $(this.maskId);
   },
   observerStart: function(){
    // safari/ie中监听keypress时,arrow keys箭头无法正确激活绑定的事件,改用keydown监听
    // Arrow keys no longer result in keypress events, now processed in keydown default event handler
    // Reference: https://lists.webkit.org/pipermail/webkit-dev/2007-December/002992.html
    // keydown/keypress区别参考ppk的文章: http://www.quirksmode.org/dom/events/keys.html
    if (Prototype.Browser.WebKit || Prototype.Browser.IE) Event.observe(this.element, 'keydown', this.renderLists.bindAsEventListener(this));
    else Event.observe(this.element, 'keypress', this.renderLists.bindAsEventListener(this));
    Event.observe(this.element, 'blur', this.onBlured.bindAsEventListener(this));
    Event.observe(document, 'mousemove', this.hideMask.bindAsEventListener(this));
   },
   addListsObserver: function(){
    var self = this;
    this.listElements.each(function(li){     
     Event.observe(li, 'mouseover', self.onMouseOverList.bindAsEventListener(self));
     Event.observe(li, 'click', self.updateField.bindAsEventListener(self));
    });
    this.hideMask();
   },
   hideMask: function() {
    if (this.maskShowed) {
     this.maskShowed = false;
     this.mask.style.height = '0px';
     this.mask.style.display = "none";
    }
   },
   showMask: function(){
    if (!this.maskShowed) {
     this.maskShowed = true;
     this.mask.style.height = this.completerDiv.clientHeight + 'px';
     this.mask.style.display = "block";
    }
   },
   onBlured: function(){
    var self = this;
    if (this.timer) clearTimeout(this.timer);
    this.timer = setTimeout(function(){
     self.hideMask();
     self.completerDiv.hide();
    }, 100);
   },
   updateField: function(){
    this.element.focus();
    if(this.listElements.length > 0 && this.index != -1)
     this.element.value = this.listElements[this.index].innerHTML.replace(/&/, "&");
    this.completerDiv.hide();
    this.hideMask();
   },
   listsShowed: function(){
    return this.completerDiv.style.display.toLowerCase() == 'block';
   },
   onSelectList: function(){
    this.listElements.each(function(li){
     li.style.backgroundColor = "#fff";     
    });
    this.listElements[this.index].style.backgroundColor = "#ffb";
   },
   divScrollTop: function() {
    var maxScrollTop = this.completerDiv.scrollHeight - this.completerDiv.clientHeight;
    var scrollBottom = this.completerDiv.clientHeight + this.completerDiv.scrollTop;
    var listHeight = this.listElements[0] ? this.listElements[0].clientHeight : 20; 
    // TODO , get li.clientHeight after insert li element and then remove it if ul list has no li child.
    if (this.index == 0) 
     this.completerDiv.scrollTop = 0;
    else if (this.index == this.listElements.length - 1)
     this.completerDiv.scrollTop = maxScrollTop;
    else if (this.index * listHeight < this.completerDiv.scrollTop)
     this.completerDiv.scrollTop = this.index * listHeight;
    else if (this.index * listHeight >= scrollBottom)
     this.completerDiv.scrollTop = this.completerDiv.scrollTop + listHeight;
   },
   markNextList: function(){
    this.showMask();
    if(this.index < this.counter - 1) this.index++
       else this.index = 0;
    this.divScrollTop();
    this.onSelectList();
   },
   markPrevList: function(){
    this.showMask();
    if(this.index > 0 ) this.index--
       else this.index = this.counter - 1;
    this.divScrollTop();
    this.onSelectList();
   },
   onMouseOverList: function(event){
    var elementLI = Event.findElement(event, 'LI');
    this.index = this.listElements.indexOf(elementLI);
    this.listElements.each(function(li){
     li.style.backgroundColor = "#fff";     
    });
    elementLI.style.backgroundColor = "#ffb";
    // this.divScrollTop();
   },
   renderLists: function(event){
    switch(event.keyCode) {
     case Event.KEY_LEFT:
     case Event.KEY_RIGHT:
      return;
     case Event.KEY_ESC:
      this.completerDiv.hide();
      this.hideMask();
      // esc will also empty input field in ie by default, prevent default event.
      Event.stop(event);
      this.index = -1;
      return;
     case Event.KEY_TAB:
     case Event.KEY_RETURN:
      this.updateField();
      Event.stop(event);
      return;
     case Event.KEY_DOWN:
      if (this.listsShowed()) {
       this.markNextList();
       return;
      }
     case Event.KEY_UP:
      if (this.listsShowed()) {
       this.markPrevList();
       return;
      }
    }
       if (this.observer) clearTimeout(this.observer);
    var self = this;
    this.observer = setTimeout(function(){self.onFieldChange(event)}, 100);    
   },
  onFieldChange: function(event){
   var value = this.element.value.strip();
   var autoWidth = this.element.getWidth();
   var find = false;
   var listItems = "";
   this.counter = 0;
   this.index = -1;
   var self = this;
   if (Prototype.Browser.IE || this.value != value || event.keyCode == Event.KEY_DOWN) {
    this.value = value;
    this.items.each(function(item){
     if (item.include(self.value)) {
      self.counter ++;
      // li element defaults line-height is different in different browser: 
      // safari 13px, firefox3 14px
      // li width must be set 100% and display block for ie, and ie scrollbar movement has related with li width, sign...
      // font size should be little than 14px in ie
      listItems += "<li style='line-height:14px;padding:3px;margin:0;display:block;width:100%;cursor:default;'>" + item + "</li>";
      if (!find) find = true;
     }
    });
   }
   if (find) {
    this.list.innerHTML = listItems;
    var pos = Position.cumulativeOffset(this.element);
    this.completerDiv.style.width = this.mask.style.width = (autoWidth - 2) + "px";
    this.completerDiv.style.left = this.mask.style.left = pos[0] + "px";
    this.completerDiv.style.top = this.mask.style.top = (pos[1] + this.element.getHeight() - 1) + "px";
    this.completerDiv.style.display = "block";
    this.listElements = this.list.childElements();
    this.addListsObserver();
   } else {
    this.completerDiv.hide();
    this.hideMask();
   }   
  }  
  });
  
  var af = new AutoFieldFromLocal('message', contacts);
  //]]>
  </script>
 </div>
</body>
</html>
需要注意一下以下几点:
1、在safari/ie中对于arrow keys事件的处理方式和keydown/keypress二者区别。
2、在ie中esc默认行为会将input框中的值一起cancel。
3、在firefox中mouseover事件的控制,在用arrow keys移动选项时,用一个蒙板挡住了mouse,以防触发mouseover事件。
4、setTimeout()方法中的this需要小心其指向全局的window。
5、当y轴出现滚动条时,其实际可以滚动的长度不是element.scrollHeight,而应该是此值减去element.clientHeight。
6、IE中控制滚动条用overflow:auto; overflow-x:hidden;
7、IE中滚动条和LI元素的宽度有关系,要控制滚动条则需要设置LI宽度为100%。
8、对&作特殊处理。