Autocompleterを読む その2

http://d.hatena.ne.jp/akm/20071128#1196263081の続きです。

今日は、動作する順番にソースコードを追ってみます。

Railsで、auto_complete_fieldメソッドを実行すると

new Ajax.Autocompleter(field_id, options[:update] || "#{field_id}_auto_complete",  url_for(options[:url]), options);

って感じで生成されるので、Ajax.Autocompleterから見ていきます。

まず、newされたら呼び出されるinitializeは、

  initialize: function(element, update, url, options) {
    this.baseInitialize(element, update, options);
    this.options.asynchronous  = true;
    this.options.onComplete    = this.onComplete.bind(this);
    this.options.defaultParams = this.options.parameters || null;
    this.url                   = url;
  },

こんな感じ。baseInitializeはAutocompleter.Base.prototypeに宣言されているメソッドです。前回書いたとおり、Ajax.Autocompleter.prototypeはAutocompleter.Base.prototypeをObject.extendされてるので、Ajax.Autocompleterは自身のメソッドのように呼び出すことができます。残りは、optionsとurlに値を設定しているだけなので、あとで考えましょう。

で、Autocompleter.Base.prototypeのbaseInitializeを見てみましょう。まずは、前半戦。

Autocompleter.Base.prototype = {
  baseInitialize: function(element, update, options) {
    this.element     = $(element); 
    this.update      = $(update);  
    this.hasFocus    = false; 
    this.changed     = false; 
    this.active      = false; 
    this.index       = 0;     
    this.entryCount  = 0;

    if(this.setOptions)
      this.setOptions(options);
    else
      this.options = options || {};

    this.options.paramName    = this.options.paramName || this.element.name;
    this.options.tokens       = this.options.tokens || [];
    this.options.frequency    = this.options.frequency || 0.4;
    this.options.minChars     = this.options.minChars || 1;

this.elementとthis.updateに引数の要素(名)から要素を設定しているところと、setOptionsというメソッドがあったら、それを呼び出しています。あとは諸々の属性とoptionsを設定してます。

baseInitialize後半戦。

    this.options.onShow       = this.options.onShow || 
      function(element, update){ 
        if(!update.style.position || update.style.position=='absolute') {
          update.style.position = 'absolute';
          Position.clone(element, update, {
            setHeight: false, 
            offsetTop: element.offsetHeight
          });
        }
        Effect.Appear(update,{duration:0.15});
      };
    this.options.onHide = this.options.onHide || 
      function(element, update){ new Effect.Fade(update,{duration:0.15}) };

    if(typeof(this.options.tokens) == 'string') 
      this.options.tokens = new Array(this.options.tokens);

    this.observer = null;
    
    this.element.setAttribute('autocomplete','off');

    Element.hide(this.update);

    Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
    Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
  },

onShow, onHide はoptionsに対して設定しておくとそれが有効になって、設定されてなかったらデフォルトの動作が設定されるのね。tokensに設定されているのがStringなら配列として設定される。observerはよくわからんけど、まあ応用編な感じでしょう。で、autocompleteをoffにしてブラウザの自動補完を切っておいて、一覧を表示するupdateを非表示に。入力フィールドからフォーカスが抜けた時と、キーが押された時のイベントハンドらをそれぞれ、onBlurとonKeyPressに設定してます。こっちはoptionsじゃないんだね。まあ、optionalな項目じゃないからだろうね。

じゃ次の処理はoptions.onShow, options.onHide, onBlur, onKeyPressですね。

まずonShowとonHideからだけど、これはデフォルトの処理は、上にある通り。まず、update.style.positionが何も設定されていないか、absoluteだったら?、style.positionをabsoluteに設定して、Position.cloneで高さの設定はせずに、offsetTopを入力フィールドのoffsetHeightに合わせて、その後updateをEffect.Appearで表示します。onHideの方は超簡単。Effect.Fadeでupdateを非表示にするだけっすね。

次、onBlur。

  onBlur: function(event) {
    // needed to make click events working
    setTimeout(this.hide.bind(this), 250);
    this.hasFocus = false;
    this.active = false;     
  }, 

フォーカスが外れると、250ms後にhideメソッドを呼び出して、フラグを設定。hideメソッドは簡単そうだからちらっと見てみましょう。

  hide: function() {
    this.stopIndicator();
    if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
    if(this.iefix) Element.hide(this.iefix);
  },

stopIndicatorはたぶん、処理中を示すgif画像を止めるってことだと思われ。で、updateがまだ表示されてたらさっきのonHideを呼び出して、this.iefixが設定されてたらそれも非表示にします。this.iefixはおそらくIE6以前のabsoluteで表示するdivなどがinputやselectと重なり合うとzIndexを指定してもinputやselectが上に表示されてしまう現象を防ぐためのiframeだと予想。まあ、そんなに重要じゃないはず。

onKeyPress。まず、キーが押された時の挙動は、this.activeによって振舞いが違います。アクティブならキーが押されれた時に反応するけど、それ以外は特に何もしません。TABとRETURNなら・・・結構面倒臭くなってきたので、ざっくりとここまでをまとめてみます。今回の僕の目的はカスタマイズすることなので、メソッドの流れを中心に書きます。ちなみに<>は条件分岐で、()は何かの処理ね。

new Ajax.Autocompleter
  Ajax.Autocompleter#initialize
    Autocompleter.Base#baseInitialize
      (派生クラス)#setOptions (もしsetOptionsがあれば)

(blurイベント発生)
  onBlur
    <250ms後>
      hide
    stopIndicator
      <表示されてたら>
        options.onHide

(keypressイベント発生)
  onKeyPress
    <activeだったら>
      <KEY_TAB or KEY_RETURN>
        selectEntry
        (イベント中止)
      <KEY_ESC>
        hide
        (イベント中止)
      <KEY_LEFT or KEY_LEFT>
        (イベントハンドラ終了)
      <KEY_UP>
        markPrevious
        render
        <Safariとかだったら>
          (イベント中止)
      <KEY_DOWN>
        markNext
        render
        <Safariとかだったら>
          (イベント中止)
    <activeじゃない>
      <KEY_TAB or KEY_RETURN or Safariとか or keyCodeが0>
        (イベントハンドラ終了)
    (observerをクリア)
    <options.frequency秒後>
      onObserverEvent
        getToken
        <入力された文字列がoptions.minChars以上>
          startIndicator
          getUpdatedChoices
        <入力された文字列がoptions.minCharsよりも短い>
          hide

(イベントハンドラ終了)は単にイベントハンドラをreturnで抜けるだけだけど、(イベント中止)はイベントをなかったことにしちゃいます。


(keypressイベント発生)以降は、まだまだ増えるでしょうけど、いまはこんな感じで。ここで思い込みメソッド予想。

メソッド名 予想
selectEntry たぶん選択肢から選んだものを入力フィールドに反映させる
markPrevious/markNext 今選択している選択肢を前後に移動させる
render 選択肢を描画する
startIndicator 今ロードしてるんだぜ、ってgifを表示
getUpdatedChoices 選択肢を更新。renderとの関係が気になるところ

長くなるので、また明日。