script.aculo.usを読み解く

第2回 controls.js(前編)Autocompleter

この記事を読むのに必要な時間:およそ 18.5 分

0213: markPrevious: function() {
0214:   if(this.index > 0) this.index--
0215:     else this.index = this.entryCount-1;
0216:   this.getEntry(this.index).scrollIntoView(true);
0217: },
0218: 
0219: markNext: function() {
0220:   if(this.index < this.entryCount-1) this.index++
0221:     else this.index = 0;
0222:   this.getEntry(this.index).scrollIntoView(false);
0223: },
0224:

213~24行目のmarkPreviousとmarkNextは,それぞれ,上下カーソルキーで,候補の選択をするときに呼ばれる関数です。

this.indexは内部的に"何番めの候補を選択中か"を表します。

216行目と222行目で,それぞれ,内部的に選択中の候補が見えるようにスクロールします。

0225: getEntry: function(index) {
0226:   return this.update.firstChild.childNodes[index];
0227: },
0228:

225~228行目のgetEntryは,引数で与えられたindex番目の候補のDOM要素を返す関数です。

0229: getCurrentEntry: function() {
0230:   return this.getEntry(this.index);
0231: },  
0232:

229~232行目のgetCurrentEntryは,内部的に選択中の候補のDOM要素を返す関数です。

0233: selectEntry: function() {
0234:   this.active = false;
0235:   this.updateElement(this.getCurrentEntry());
0236: },
0237:

233~237行目のselectEntryは,候補を確定する関数です。

234行目で,候補メニューを非表示にするフラグを立てています。

235行目で,実際に入力エリアの内容を書き換えて補完するupdateElement関数を呼んでいます。

0238: updateElement: function(selectedElement) {
0239:   if (this.options.updateElement) {
0240:     this.options.updateElement(selectedElement);
0241:     return;
0242:   }
0243:   var value = '';
0244:   if (this.options.select) {
0245:     var nodes = $(selectedElement).select('.' + this.options.select) || [];
0246:     if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
0247:   } else
0248:     value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
0249:

238~266行目のupdateElementは,候補を確定したときに入力エリアの内容を書きかえる関数です。

239行目で,更新前のフックthis.options.updateElementが用意されています。

243~249行目で,<ul>や<li>のDOMツリーをたどって文字列を取り出すために,effect.js内のElement.collectTextNodeやElement.collectTextNodesIgnoreClassが使われます。

このとき,this.options.selectというオプションが関わってきます。このオプションを例えば"selectme"とすると,

245行目で,Element.selectメソッドを'.selectme'を引数にして呼び,そのCSSセレクタに一致するノードを集めます。

246行目で,Element.collectTextNodeによって,クラスが"selectme"のノードの文字列を集めます。例えば,以下の候補を確定したときfoobarを出力します。

<li>
  無駄無駄
  <span class="selectme">foobar</span>
</li>

248行目で,このオプションが設定されていないとき,Element.collectTextNodesIgnoreClassによって,クラスが"informal"のノードの文字列を無視します。つまり,以下の候補を確定したときfoobarを出力します。

<li>
  <span class="informal">無視無視</span>
  foobar
</li>
0250:   var bounds = this.getTokenBounds();
0251:   if (bounds[0] != -1) {
0252:     var newValue = this.element.value.substr(0, bounds[0]);
0253:     var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
0254:     if (whitespace)
0255:       newValue += whitespace[0];
0256:     this.element.value = newValue + value + this.element.value.substr(bounds[1]);
0257:   } else {
0258:     this.element.value = value;
0259:   }
0260:   this.oldElementValue = this.element.value;
0261:   this.element.focus();
0262: 
0263:   if (this.options.afterUpdateElement)
0264:     this.options.afterUpdateElement(this.element, selectedElement);
0265: },
0266:

250行目までで,候補のテキストを取り出せたので,次にそれを入力エリアに挿入します。

250行目で,getTokenBounds関数で,候補を挿入する位置を調べます。この関数は,トークンの範囲を[始点,終点]の配列で返してきます。

251~259行目で,ここでbounds[0]==-1であれば,入力エリアが空ということなので,単純に上書きをします。そうでなければ,入力エリアが空ではないということなので,入力エリアの既存の内容にテキストを挿入します。

253~255行目で,このトークンの範囲は,前方に空白を含んで計算されているので,その分を補ってやります。

260行目で,oldElementValueを入力エリアの内容に更新します。

261行目で,フォーカスを入力エリアにあたえます。

263行目で,更新後のフックthis.options.afterUpdateElementが用意されています。

0267: updateChoices: function(choices) {
0268:   if(!this.changed && this.hasFocus) {
0269:     this.update.innerHTML = choices;
0270:     Element.cleanWhitespace(this.update);
0271:     Element.cleanWhitespace(this.update.down());
0272:  
0273:     if(this.update.firstChild && this.update.down().childNodes) {
0274:       this.entryCount = 
0275:         this.update.down().childNodes.length;
0276:       for (var i = 0; i < this.entryCount; i++) {
0277:         var entry = this.getEntry(i);
0278:         entry.autocompleteIndex = i;
0279:         this.addObservers(entry);
0280:       }
0281:     } else { 
0282:       this.entryCount = 0;
0283:     }
0284:  
0285:     this.stopIndicator();
0286:     this.index = 0;
0287:   
0288:     if(this.entryCount==1 && this.options.autoSelect) {
0289:       this.selectEntry();
0290:       this.hide();
0291:     } else {
0292:       this.render();
0293:     }
0294:   }
0295: },
0296:

267~296行目のupdateChoicesは,choicesという引数で候補のリストのHTMLを受け取って,処理する関数です。

例えば,ここでchoicesは以下のようなものです。

<ul>
  <li>
    foo
  </li>
  <li>
    <span class="informal">無視無視</span>
    foobar
  </li>
</ul>

270行目と271行目で,this.updateやその子ノードの<ul>...から,無駄な空白を取り除いていますElement.downメソッドは,子孫ノードを返します⁠⁠。

278,279行目で,各<li>要素に,番号を振り,イベントハンドラを設定しています。

285行目で,インジケータを非表示にします。

286行目で,先頭の候補を内部的な選択中にします。

288行目で,候補がひとつだけで,かつ,options.autoSelectが設定されているときは,自動で確定します。

0297: addObservers: function(element) {
0298:   Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
0299:   Event.observe(element, "click", this.onClick.bindAsEventListener(this));
0300: },
0301:

297~301行目のaddObserversは,候補メニューのDOM要素に,カーソルが入ったとき(onHover)と,クリックされたとき(onClick)の,イベントハンドラを設定しています。

0302: onObserverEvent: function() {
0303:   this.changed = false;   
0304:   this.tokenBounds = null;
0305:   if(this.getToken().length>=this.options.minChars) {
0306:     this.getUpdatedChoices();
0307:   } else {
0308:     this.active = false;
0309:     this.hide();
0310:   }
0311:   this.oldElementValue = this.element.value;
0312: },
0313:

302~313行目は,onObserverEvent関数です。これはAutocompleterの心臓部です。キャレットのおおまかな位置を求めて,トークンを切り出して,トークンから候補を導きます。

304行目で,tokenBoundsをnullにしてからgetTokenを呼ぶことで,改めて,キャレットの位置の計算とトークンの切りだしが行われます(getTokenは,無駄な処理を減らすために,tokenBoundsの結果が残っているときはそれをそのまま返すようになっています⁠⁠。

305行目で,切り出したトークンの長さがminCharsを満たすときだけ,候補の検索を行っています。

306行目で,getUpdatedChoicesを呼んでいます。この関数は,Ajax.Autocompleterでは,サーバへの問い合わせを,Autocompleter.Localでは,ローカルに与えられた語の配列からの検索を,それぞれ行います。

308,309行目で,minCharsに満たなかったときは,候補を非表示にしています。

311行目で,入力エリアの内容を保存しておきます。これがあとで,キャレットの位置を求めたり,Ajaxが失敗したときに内容を復帰するのに使われます。

著者プロフィール

源馬照明(げんまてるあき)

名古屋大学大学院多元数理科学研究科1年。学部生のときにSchemeの素晴らしさを知ったのをきっかけに,関数型言語の世界へ。JavaScriptに,ブラウザからすぐに試せる関数型言語としての魅力と将来性を感じている。

ブログ:Gemmaの日記