end0tknr's kipple - 新web写経開発

http://d.hatena.ne.jp/end0tknr/ から移転しました

jqueryを使ってsuggest (autocomplete? 入力補完?)を書いてみる

suggest機能のjavascript実装はたくさんあります

最近では、suggest機能(autocomplete機能?)もよく見かけるようになり、簡単に調べるだけでも多くのライブラリを見つけることができます。

suggest.js
http://www.enjoyxstudy.com/javascript/suggest/
jquery Autocomplete
http://docs.jquery.com/Plugins/Autocomplete
Ajax Auto Suggest
http://www.brandspankingnew.net/archive/2007/02/ajax_auto_suggest_v2.html
jquery suggest
http://www.vulgarisoip.com/2007/06/29/jquerysuggest-an-alternative-jquery-based-autocomplete-library/

先程のjavascriptに対する不満?

先程のjavascriptライブラリを使って、suggest機能を実現しても構いませんが、 いくつか不満な点がありました。

  • 複雑な検索条件をserverへリクエストしたい。
  • 入力候補はselectタグで表示して、表示値と実際の値を分離したい。
  • 入力候補はselectタグで表示して、候補数が多い場合も画面が壊れない。


以上のようなことを考えていたところ、gihyo.jpの「jQueryではじめるAjax」の「第4回 検索キーワードを提案するSuggest機能の実装」で、suggest機能の実装が記載されていたので、今回はその記事を参考にsuggest機能を実装してみました。
http://gihyo.jp/dev/feature/01/jquery-ajax/0004?page=1

画面サンプル

今回、作成したsuggestは製品品番?の各桁入力する為のものです。
text box内でspaceキーを押すと、1桁目からの入力内容に応じた各桁の入力候補が表示されます。suggestはselect tagで表示していますが、selectの先頭には桁のtitle?を表示しています。また、suggest内でspaceキーを押した場合、focusが次の入力候補に移動します。

src

先程の画面サンプルのsrcです。とりあえず、動作していますが、間違いがあれば、そのうち、修正します。

html
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="default.css">
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="ui.core.js"></script>
<script type="text/javascript" src="ui.sortable.js"></script>
</head>

<body>
<form>

<div class="container">

<h1>SUGGEST HINBAN</h1>

<table>
  <thead>
  <tr>
    <th>NO</th><th>HINBAN GROUP</th><th>HINBAN CODE</th>
    <th>QUANTITY</th><th>NOTE</th><th></th>
  </tr>
  </thead>
  <tbody id="mim_tbody">
  </tbody>
</table>

<input type="button" id="row_add" value="+ : add row"
 onClick="edit_row.add_row()">

<div class="note">
※SPACE key :
 「suggest hinban group , mim」or「focus to next suggest option」.<br>
※ESC key : hide suggest.<br>
※You can drag an edit row to a new spot.
</div>

<table style="visibility:hidden; display:none">
  <tbody id="mim_tbody_template">
  <tr>
    <td class="row_no"></td>
    <td><input type="text" class="mim_group" name="mim_group" size="20"
	maxlength="30" autocomplete="off">
    <td><input type="text" class="mim" name="mim" size="20"
	maxlength="15" autocomplete="off">
    </td>
    <td><input type="text" class="quantity" name="quantity" size="6"
	value="1.0" maxlength="30" autocomplete="off">
    </td>
    <td class="mim_note"></td>
    <td>
	<input type="button" class="row_copy" onClick="edit_row.copy_row(this)"
	value="+ : copy row">
	<input type="button" class="row_del" onClick="edit_row.del_row(this)"
	value="× : delete row">
    </td>
  </tr>
  </tbody>
</table>

<select id="suggest" style="visibility:hidden"></select>
</div><!--container---->
</form>
<script type="text/javascript" src="SuggestMim.js"></script>
</body>
</html>
javascript
var MIM_DEF =
    { //HINBAN GROUP
        HR:{
            name: '梁',
            col_all: [ //コード説明書の1ページ目に該当
                //start end     name
                [1,     2,      '部品種(固定)'],
                [3,     4,      '長さ'],
                [5,     5,      '厚さ'],
                [6,     6,      '向き']
            ],
            col: { //各桁のバリエーション
                //  code    note      disp_no
                1: [['HR',  '部品種', 1]],
                3: [['09',  '900mm',  1],
                    ['18',  '1800mm', 2]],
                5: [['U',   '薄い',   1],
                    ['A',   '厚い',   2]],
                6: [['L',   'L',      1],
                    ['R',   'R',      2],
                    ['*',   'その他', 3]]
            },
            mims: [ //登録済HINBAN
                //hinban          name
                ['HR09UL',        '梁 type1'],
                ['HR09UR',        '梁 type2'],
                ['HR09U*',        '梁 type3'],
                ['HR09AL',        '梁 type4'],
                ['HR09A*',        '梁 type5'],
                ['HR18UL',        '梁 type6'],
                ['HR18UR',        '梁 type7'],
                ['HR18U*',        '梁 type8'],
                ['HR18AR',        '梁 type9'],
                ['HR18AL',        '梁 type10']
            ]
        },
        HS:{
            name: '柱',
            col_all: [
                //start end     name
                [1,     2,      '部品種(固定)'],
                [3,     4,      '長さ'],
                [5,     5,      '厚さ'],
                [6,     6,      '向き']
            ],
            col: {
                //  code     note        disp_no
                1: [['HS',   '部品種',   1]],
                3: [['09',   '900mm',    1],
                    ['18',   '1800mm',   2]],
                5: [['U',    '薄い',     1],
                    ['A',    '厚い',     2]],
                6: [['L',    'L',        1],
                    ['R',    'R',        2],
                    ['*',    'その他',   3]]
            },
            mims: [
                //mim              name
                ['HS09UL',        '柱 type1'],
                ['HS09UR',        '柱 type2'],
                ['HS09U*',        '柱 type3'],
                ['HS09AL',        '柱 type4'],
                ['HS09A*',        '柱 type5'],
                ['HS18UL',        '柱 type6'],
                ['HS18UR',        '柱 type7'],
                ['HS18U*',        '柱 type8'],
                ['HS18AR',        '柱 type9'],
                ['HS18AL',        '柱 type10']
            ]
        }
    };


(function() {

    var Suggest = function() {};
    
    Suggest.prototype = {
        tid: null,         // suggest timer id
        SUGGEST_TIME: 200, //suggestまでの時間(msec)
        
        clear_suggest: function(){
            $("#suggest").html('');
            $("#suggest").css('visibility','hidden');
            this.suggest_to.focus();
            set_caret_pos( this.suggest_to );
        },
        
        disp_suggest: function (suggests, org_elm){
            if(suggests.length < 1){
                return;
            }
            sgst.suggest_to = org_elm;

            for(var i=0; i<suggests.length; i++) {
                $("#suggest").append(
                    $("<option></option>").val(suggests[i][0])
                        .html(suggests[i][1])
                );
            }

            $("#suggest").attr('size','10');
            $("#suggest").css('position','absolute');
            
            //drop down menu表示
            var pos_x = $(org_elm).offset().left;
            var pos_y = $(org_elm).offset().top + $(org_elm).height();
            $("#suggest").css('left', pos_x + 'px');
            $("#suggest").css('top',  pos_y + 'px');

            $("#suggest").css('visibility','visible');

            $("#suggest").unbind('dblclick');
            $("#suggest").bind('dblclick',
                               function(event){
                                   sgst.decide_suggest(event.target);
                               });
	    $("#suggest").focus()[0].selectedIndex = 1;
        },        

        decide_suggest: function(suggest_elm){
            var sgst_to = sgst.suggest_to;
            if (suggest_elm.value.length > 0){
                sgst_to.value = suggest_elm.value || '';
            }
            sgst.clear_suggest();
            sgst_to.focus();
            set_caret_pos( sgst_to );
        },

        stop_suggest: function() {
            clearTimeout(this.tid);
        },

        suggest: function(org_elm){
            //keyが押される毎にはsuggestしません. (2重submit防止?)
            this.stop_suggest();

            if (org_elm.className=='mim_group'){
                this.tid = setTimeout(this.suggest_mim_group,
                                      this.SUGGEST_TIME,
                                      org_elm,
                                      this);
            } else if (org_elm.className=='mim'){
                this.tid = setTimeout(this.suggest_mim,
                                      this.SUGGEST_TIME,
                                      org_elm,
                                      this);
            }
        },
        
        suggest_mim_group: function(org_elm,self){
            var suggests = [['','[HINBAN GROUP]']];
            var reg_exp = new RegExp(org_elm.value, "i");
            for (var i in MIM_DEF){
                if (i.search(reg_exp) != -1 ||
                    MIM_DEF[i].name.search(reg_exp) != -1) {
                    var suggest_val = suggest_name = i+' '+MIM_DEF[i].name;
                    suggests.push([suggest_val,suggest_name]);
                }
            }
            self.disp_suggest(suggests,org_elm);
        },

        suggest_mim : function(org_elm,self){
            var row_id = $('.mim').index(org_elm);
            var mim_grp =  $('.mim_group').eq(row_id).val();
            var mim_grp_id = mim_grp.substr(0,2);

            if ( self.exist_mim_group_id(mim_grp_id) == false ){
                alert(mim_grp +' に該当するHINBAN GROUPが見つかりませんでした');
                return;
            }
            
            var search_str = org_elm.value.toUpperCase();
            var mims =  MIM_DEF[mim_grp_id].mims;

            if (search_str.length >= mims[0][0].length ){
                org_elm.value = search_str.substr(0,mims[0][0].length);
                var suggests = [['', '※必要なMIM長さに達しました']];
                self.disp_suggest(suggests,org_elm);
                self.chk_input_mim(org_elm);
                return;
            }

            var col_pos_def = self.get_mim_col_pos_def(mim_grp_id,search_str);
            if (col_pos_def.length == 0 ){
                alert('検索対象のHINBAN桁定義が見つかりませんでした');
                return;
            }

            search_str = search_str.substr(0,col_pos_def[0]);
            //その桁?が何桁で構成されるか
            var col_len = col_pos_def[1] - col_pos_def[0] + 1;
            var col_pat_list = MIM_DEF[mim_grp_id]['col'][col_pos_def[0]];
            var col_pat_hash = self.mim_col_list2hash(col_pat_list);

            var search_str_base = search_str.substr(0,col_pos_def[0]-1);
            
            var reg_exp = new RegExp("^"+search_str, "i");
            var suggests = [['', '['+col_pos_def[2]+']']];
            var suggested_col = {};     //発見したバリエーション数
            for (var i in mims){
                if (suggests.length >= col_pat_list.length + 1)  break;
                if ( mims[i][0].search(reg_exp) < 0) continue;

                var sgst_col = mims[i][0].substr(search_str_base.length,col_len);
                if (suggested_col[sgst_col] > 0 ) continue;

                suggested_col[sgst_col] = 1;
                var sgst_val = search_str_base + sgst_col;
                var sgst_disp =
                    search_str_base+sgst_col+' '+ col_pat_hash[sgst_col][1];
                suggests.push([sgst_val, sgst_disp]);
            }

            //TODO ここにsuggestしたMIMのsortを追加してもいいかも

            self.disp_suggest(suggests,org_elm);
        },

        chk_input_mim : function(org_elm){
            var row_id = $('.mim').index(org_elm);
            var mim_grp =  $('.mim_group').eq(row_id).val();
            var mim_grp_id = mim_grp.substr(0,2);

            if ( this.exist_mim_group_id(mim_grp_id) == false ){
                $('.mim_note').eq(row_id)
                    .html("<span class='error'>"+
                          "HINBAN GROUPがHINBANマスタに登録なし"+"</span>");
                return;
            }
            
            var search_str = org_elm.value = org_elm.value.toUpperCase();
            var mims =  MIM_DEF[mim_grp_id].mims;
            
            if (search_str.length >= mims[0][0].length ){
                search_str = org_elm.value =
                    search_str.substr(0,mims[0][0].length);
            }

            for (var i in mims){
                if(mims[i][0] != search_str) continue;
                $('.mim_note').eq(row_id).html(mims[i][1]);
                return;
            }
            $('.mim_note').eq(row_id)
                    .html("<span class='error'>"+
                          "HINBANマスタに登録なし"+"</span>");
        },

        get_mim_col_pos_def: function(mim_group,search_str){
            var search_pos = search_str.length+1;
            for (var i in MIM_DEF[mim_group]['col_all']){
                if(MIM_DEF[mim_group]['col_all'][i][0] <= search_pos &&
                    search_pos <= MIM_DEF[mim_group]['col_all'][i][1]){
                    return MIM_DEF[mim_group]['col_all'][i];
                }
            }
            return [];
        },

        exist_mim_group_id: function(mim_grp_id){
            for (var i in MIM_DEF){
                if (mim_grp_id == i) return true;
            }
            return false;
        },

        mim_col_list2hash: function(col_pat_list){
            var col_hash = {};
            for (var i in col_pat_list){
                col_hash[col_pat_list[i][0]] = col_pat_list[i];
            }
            return col_hash;
        }
    };
    // 名前空間 window に公開
    window.sgst = new Suggest();
})();

function blur_note(event){
    sgst.chk_input_mim(event.target);
}

function act_key_down(event){

    var org_elm = event.target;
    if ( event.keyCode == '27') {    //27=ESC
        if ($("#suggest").css("visibility") != 'hidden') {
            sgst.clear_suggest();
        }
        return;
    }
    if ( event.keyCode == '13' && org_elm.id == 'suggest' ) {	//13=enter
        sgst.decide_suggest(org_elm);
	return;
    }

    if(event.keyCode == '32' && //32=space
       org_elm.id == 'suggest'){
        var selected_idx = $("#suggest").focus()[0].selectedIndex;
        if ( (selected_idx+1) >= $("#suggest option").size() ){
            $("#suggest").focus()[0].selectedIndex = 1;
        } else {
            $("#suggest").focus()[0].selectedIndex++;
        }
    }


    if((event.keyCode == '32' ||         //32=半角space
        event.keyCode == '229') &&       //229=全角space
       (org_elm.className == 'mim' ||
        org_elm.className == 'mim_group') ) {
        event.preventDefault(); //stop default event    

        if ($("#suggest").css("visibility") != 'hidden') {
	    $("#suggest").focus()[0].selectedIndex = 1;
            return;
	}

        sgst.suggest(org_elm);
//        var suggest_method = 'sgst.suggest_' + org_elm.className +'(org_elm)';
//        sgst.tid = setTimeout(suggest_method, sgst.SUGGEST_TIME);
//        eval( suggest_method );
        return;
    }
}

function EditRow() {
    this.add_row = function(){
        var new_tr = $('#mim_tbody').append($('#mim_tbody_template tr').clone());
        $('input.mim_group',new_tr).focus();
        edit_row.renum();
    };
    this.del_row = function(org_elm){
        var index = $('.row_del').index(org_elm);
        $('#mim_tbody tr').eq(index).remove();
        edit_row.renum();
    };
    this.copy_row = function(org_elm){
        var idx = $('.row_copy').index(org_elm);
        var target_row = $('#mim_tbody tr').eq(idx);
        var new_row = target_row.after(target_row.clone());
        edit_row.renum();
        $('#mim_tbody input.mim_group').eq(idx+1).focus();
    };
    this.renum = function(){
        $('.row_no').each(function(i,elm){
            $(elm).html(parseInt(i+1));
        });
    };
}

var edit_row = new EditRow;
$("#mim_tbody").sortable({cursor: 'move',
                          stop: function(event,ui) { edit_row.renum(); }
});

function get_caret_pos( elm ) {

    if (elm.selectionStart != undefined){
        return elm.selectionStart;
    }
    //for IE
    elm.focus();
    var range = document.selection.createRange();
    range.moveStart( "character", - elm.value.length );
    return range.text.length;
}

function set_caret_pos( elm ) {
    if (elm.createTextRange){
        var range = elm.createTextRange();
        range.move('character', elm.value.length);
        range.select();
    }
}

$("form").live("keydown", function(event){act_key_down(event);});
$(".mim").live("blur",   function(event){blur_note(event);});
$("#row_add").focus();
style sheet
html, body, div, h1, h2, h3, h4, h5, h6, ul, ol, li {
    margin: 0;
    padding: 0;
}

li {
    list-style-type: none;
}

A:link { color: #0000cc; }
A:visited { color: #0000cc; }
A:active { color: #0000cc; }
A:hover { color: #FF0000; }

html {
    overflow:auto;
}

h2 {
  font-size: large;
  background-color: #ADBAFF;
  margin-top: 2px;
  margin-bottom: 5px;
  padding: 2px;
}

h3 {
  font-size: medium;
  border-top: 2px solid #ADBAFF;
  border-left: 5px solid #ADBAFF;
  margin-bottom: 2px;
}

h4 {
    font-size: medium;
    border-left: 5px solid #ADBAFF;
    margin-top: 5px;
    margin-bottom: 5px;
}

form {
    display: inline;
}

table {
    border-collapse: collapse;
}

th {
    font-weight: normal;
    border: 1px solid #000000;
    background-color: #ADBAFF;
}

td {
    border: 1px solid #000000;
}

.mim_group, .mim, .quantity, .unit_no {
    ime-mode: disabled;
    font-family: monospace;
}

.container {
    padding: 10px;
}

.row_copy, .row_del {
    font-size: small;
    cursor: pointer;
}

input.quantity {
    text-align: right;
    padding-right: 1px;
}
.row_no {
    text-align: right;
    cursor: move;
}
.error {
    color: #FF0000;
}
.note {
    font-size: small;