script.aculo.usを読み解く

第2回 controls.js(前編)Autocompleter

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

0409: Autocompleter.Local = Class.create(Autocompleter.Base, {
0410:   initialize: function(element, update, array, options) {
0411:     this.baseInitialize(element, update, options);
0412:     this.options.array = array;
0413:   },
0414:

410~414行目は,initializeです。3番めの引数で補完したい語の配列をとります。

0415: getUpdatedChoices: function() {
0416:   this.updateChoices(this.options.selector(this));
0417: },
0418:

415~418行目は,getUpdatedChoicesです。this.options.selectorが,候補リストのHTML表現を返し,updateChoicesが,それをメニューに表示します。

0419: setOptions: function(options) {
0420:   this.options = Object.extend({
0421:     choices: 10,
0422:     partialSearch: true,
0423:     partialChars: 2,
0424:     ignoreCase: true,
0425:     fullSearch: false,
0426:     selector: function(instance) {
0427:       var ret       = []; // Beginning matches
0428:       var partial   = []; // Inside matches
0429:       var entry     = instance.getToken();
0430:       var count     = 0;
0431:  
0432:       for (var i = 0; i < instance.options.array.length &&  
0433:         ret.length < instance.options.choices ; i++) { 
0434:  
0435:         var elem = instance.options.array[i];
0436:         var foundPos = instance.options.ignoreCase ? 
0437:           elem.toLowerCase().indexOf(entry.toLowerCase()) : 
0438:           elem.indexOf(entry);
0439:  
0440:         while (foundPos != -1) {
0441:           if (foundPos == 0 && elem.length != entry.length) { 
0442:             ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + 
0443:               elem.substr(entry.length) + "</li>");
0444:             break;
0445:           } else if (entry.length >= instance.options.partialChars && 
0446:             instance.options.partialSearch && foundPos != -1) {
0447:             if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
0448:               partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
0449:                 elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
0450:                 foundPos + entry.length) + "</li>");
0451:               break;
0452:             }
0453:           }
0454:  
0455:           foundPos = instance.options.ignoreCase ? 
0456:             elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : 
0457:             elem.indexOf(entry, foundPos + 1);
0458:  
0459:         }
0460:       }
0461:       if (partial.length)
0462:         ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
0463:       return "<ul>" + ret.join('') + "</ul>";
0464:     }
0465:   }, options || { });
0466: }
0467:});
0468:

419~468行目は,setOptionsです。このクラスに特有のオプションを追加する関数です。

ここのselectorに,候補を導くロジックが納められていて,Autocompleter.Localのもっとも重要な部分です。String.indexOf()で,語をひとつひとつ調べていきます。indexOfが0なら,前方一致です。indexOfが1以上なら,単語別前方一致の可能性がでてきます。ただし,fullSearchが有効なら,indexOfが1以上なら部分一致がなりたつので,もう単語別前方一致の可能性を考慮する必要はありません。

436行目で,foundPosはindexOfによって,一致がなければ-1に,前方一致であれば0に,どこかに一致があれば1以上になります。

440行目からのループは,単語別前方一致を見つけるためのループです。単なる前方一致や部分一致なら,このループは一度しか回りません。なぜ単語別前方一致の検索にループが必要かというと,例えば'bar afoo foobar'.indexOf('foo')のとき,まずはafooが検出されますが,これはただの部分一致なのでそれをスキップして,次のfoobarを見に行くためです。次のfoobarの1文字前がスペース(正規表現の\s)で単語であるので,単語別前方一致となり,ループを抜けます。

見つかった候補のリストは,HTMLの<ul><li>...</li>...</ul>の表現にして返します。特に,トークンと一致している部分は<strong>で強調表示します。

著者プロフィール

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

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

ブログ:Gemmaの日記