Textbox の Auto Complete (suggest っぽいの)

AutoComplete - MochiKit - Trac を見てもピンと来なかったので書いてみた。
汎用性無し、機能不足、負荷対策をしていないので、改良の余地がありますが、まー、雛形って事で。

autocomplete.html

<html>
<head>
  <script type="text/javascript" src="/path/to/MochiKit.js"></script>
  <script type="text/javascript" src="/path/to/autocompleter.js"></script>
  <script type="text/javascript">
    addLoadEvent(
      function () {
        new AutoCompleter("text", "select", ['dog', 'door', 'doctor', 'dock']);
      }
    );
  </script>
  <style type="text/css" media="all">
  #text {
    width: 10em;
  }

  #select {
    position: absolute;
    margin: 0;
    padding: 0;
    width: 10em;
    background-color: #FFFFFF;
    border: 1px solid #CCCCFF;
  }

  #select li {
    list-style-type: none;
    overflow: hidden;
    white-space: nowrap;
    font-size: 90%;
  }

  #select li.choice {
    color: #FFFFFF;
    background-color: #3366FF;
  }
  </style>
</head>
<body>
<input id="text" type="text" name="text" maxlength="10" autocomplete="off">
<ul id="select"></ul>
</body>
</html>

ul と li タグを使って選択用のボックスを表示します。
選択中の要素(li タグ)には、choice というクラス名を付けているので、CSS による装飾が必要です。

autocompleter.js

var AutoCompleter = function (input_id, select_id, select_items) {
  bindMethods(this);

  this.input_elem = $(input_id);
  this.select_elem = $(select_id);
  this.select_items = $(select_items);

  this.prev_text = '';
  this.hide_lock = false;

  this.highlight_elem = partial(this.set_class_name, 'choice');
  this.clear_highlight_elem = partial(this.set_class_name, '');

  connect(this.input_elem, 'onblur', this.blur);
  connect(this.input_elem, 'onkeyup', this.keyup);
  connect(this.input_elem, 'onkeydown', this.keydown);

  this.hide_select();
};

AutoCompleter.prototype = {
  blur: function (e) {
    this.hide_select();
  },

  keyup: function (e) {
    this.check(e.key().string);
  },

  keydown: function (e) {
    this.choice(e.key().string);
  },

  mouseover: function (index, e) {
    this.hide_lock = true;
    this.clear_highlight_elem();
    this.choice_index = index;
    this.highlight_elem();
  },

  mouseout: function (index, e) {
    this.hide_lock = false;
  },

  click: function (index, e) {
    this.choice_index = index;
    this.set_text();
    this.hide_lock = false;
    this.hide_select();
  },

  hide_select: function () {
    if (this.hide_lock) {
      return;
    }
    this.show_items = null;
    this.show_item_elems = null;
    this.choice_index = null;
    hideElement(this.select_elem);
  },

  show_select: function () {
    if (   isUndefinedOrNull(this.show_item_elems)
        || this.show_item_elems.length === 0
    ) {
      return;
    }
    replaceChildNodes(this.select_elem, this.show_item_elems);
    showElement(this.select_elem);
  },

  check: function (key) {
    var control_keys = ['ARROW_UP', 'ARROW_DOWN', 'ENTER', 'ESCAPE'];
    for (var i in control_keys) {
      if (key === 'KEY_' + control_keys[i]) {
        return;
      }
    }

    var text = this.get_text();
    if (text === this.prev_text) {
      return;
    }

    this.prev_text = text;
    this.search(text);
  },

  get_text: function () {
    return this.input_elem.value;
  },

  set_text: function () {
    if (
         isUndefinedOrNull(this.choice_index)
      || isUndefinedOrNull(this.show_items[this.choice_index])
    ) {
      return;
    }
    this.input_elem.value = this.show_items[this.choice_index];
  },

  search: function (text) {
    this.hide_select();
    if (isEmpty(text)) {
      return;
    }

    this.show_items = filter(partial(this.filter_item, text), this.select_items);
    this.show_item_elems = map(partial(LI, null), this.show_items);

    var methods = ['click', 'mouseover', 'mouseout'];
    for (var i in this.show_item_elems) {
      for (var j in methods) {
        connect(
          this.show_item_elems[i],
          'on' + methods[j],
          partial(this[methods[j]], parseInt(i))
        );
      }
    }

    this.show_select();
  },

  filter_item: function (text, item) {
    var pos = item.toLowerCase().indexOf(text.toLowerCase());
    if (pos === -1) {
      return false;
    }
    return true;
  },

  choice: function (key) {
    switch (key) {
      case 'KEY_ARROW_UP':
        this.change_choice(-1);
        break;
      case 'KEY_ARROW_DOWN':
        this.change_choice(1);
        break;
      case 'KEY_ENTER':
        this.set_text();
        this.hide_select();
        break;
      case 'KEY_ESCAPE':
        this.hide_select();
        break;
    }
  },

  change_choice: function (increment) {
    if (isUndefinedOrNull(this.show_item_elems)) {
      return;
    }
    this.clear_highlight_elem();
    this.set_choice_index(increment);
    this.highlight_elem();
  },
    
  set_choice_index: function (increment) {
    if (isNull(this.choice_index)) {
      this.choice_index = 0;
      return;
    }

    var index = this.choice_index + increment;
    if (index < 0 || this.show_item_elems.length <= index) {
      return;
    }

    this.choice_index = index;
  },

  set_class_name: function (name) {
    if (
         isUndefinedOrNull(this.choice_index)
      || isUndefinedOrNull(this.show_item_elems[this.choice_index])
    ) {
      return;
    }
    setElementClass(this.show_item_elems[this.choice_index], name);
  }
};