プルダウンリストの3連動

[新着] TAG indexオフライン版 3.0 を準備中です



0   名前: 佐々木 : 2007/04/15(日) 21:22  ID:rE6Z/9aB sub-Bq
 インターネットで調べてみたのですが、プルダウンの3連動処理をJSで出来るでしょうか?
動作は以下のイメージです。

@1つめのプルダウンで「東京都」を選択
A2つめのプルダウンでは@で選択した項目に該当する「品川区・豊島区・江東区…」が表示され、
 「豊島区」を選択
B3つめのプルダウンには、@Aで選択した「東京都/豊島区」に該当する「池袋・○○・××…」が表示

 
言葉不足でしたら申し訳ありません…。またこのときに初期値を動的に"selected"で設定する事は可能でしょうか?

1   名前: 匿名 : 2007/04/15(日) 21:22  ID:fx/kxuA4 sub-Ds
>出来るでしょうか?

出来ます。

>インターネットで調べてみたのですが

丁寧に説明しているサイトはいくらでもありますよ。

2   名前: 匿名 : 2007/04/15(日) 21:22  ID:5vHRQeeI sub-Cz
つい最近同じ話題があったのだが、過去ログ検索くらいはしたのだろうか。
http://www.tagindex.com/cgi-lib/q4bbs/patio.cgi?mode=view&no=1066

手前味噌になるが、HTML データを編集するだけで好きなだけ連動できる(一応、XForms 1.0 の switch 要素を参考にした)。バグはいくつかあるけどね。

3   名前: 佐々木 : 2007/04/15(日) 21:22  ID:rE6Z/9aB sub-Bq
>匿名さん

 ごもっともなご指摘ありがとうございます。以後気をつけます…。

 上記サイトを見ていてもやはり、初期値を動的に制御できるようにはなっていませんでした。
プルダウンリスト内の「-」をなくして設定した初期値を表示させたいのですが…。
よろしく願い致します。

4   名前: 匿名 : 2007/04/15(日) 21:22  ID:q86ngjJh sub-Cz
> 設定した初期値を表示させたい

逆に聞くが、どんな方法を考えてみた? アドバイスはするが、ここは制作代行ではないので一応。



最初から selected 属性を複数指定できればベストなんだが、そうするには select 要素を multiple="multiple" にしなければならず、却下。

ならば、少々不格好だが、
<optgroup label="SHARP">
  <option>MZ-80K</option>
  <option class="selected">MZ-700</option>
  <option>MZ-2500</option>
</optgroup>
<optgroup label="NEC">
  <option>PC-6001</option>
  <option>PC-8001</option>
  <option class="selected">PC-9801</option>
  <option>PC-9821</option>
</optgroup>
と、class="selected" を与えておく。これを初期選択項目と考える。



さて、関数 filterOptions の最後に
select.selectedIndex = 0;
の一文がある。これが初期選択される option 要素の番号だ。現在は 0 番目、つまり最初のもので固定されている。ここに、0 ではなく、class="selected" を持つ option 要素の番号を入れてやれば良い。

では、class="selected" を持つ option 要素を調べるにはどうするか。今のままだと少々面倒かもしれない。
function filterSelects (select) {
    //...snip...
    
    while (i < I) {
        var label = clone.options[ clone.selectedIndex ].text;
        var clone = recoverControl (form, RelationOrder[i++]);
        filterOptions (clone, label);
    }
}

function filterOptions (select, label) {
    //...snip...
    
    select.selectedIndex = getIndexOfSelectedOption (select);
}

function getIndexOfSelectedOption (select) {
    var options = select.getElementsByTagName ('option');
    for (var i = options.length; --i; ) {
        if (/ selected /.test (' ' + options[i].className + ' ')) break;
    }
    return i;
}

もっとスマートに行けそうな気がしたので、改訂版を載せるかもしれないし、載せないかもしれない。

もちろん、こんなスクリプトを使う義理はない。>>1 が言うように、タイトルをそのまま検索するだけで、サンプルは腐るほど見つかる。どれを選ぶかは自由だ。

5   名前: 匿名 : 2007/04/15(日) 21:22  ID:jRk5Y/ta sub-Cz
試作品。Firefox 1.5+ なら動作する。Opera 9.0+、Safari 2.0+ では、ちょっと手を加えれば連動はするが、optgroup 要素の disabled 属性に対応してないので非表示にはならない。独自拡張の塊の IE は、現時点では眼中にない。

なお、text/html ではなく application/xhtml+xml または application/xml でパースされることが大前提なので、ローカルで試す場合はファイル拡張子を .xhtml にすると良い。.html だとパースエラーになるはず(どうしても IE で開きたければ .xml にすれば良いが、どうせ動作はしない)。
<?xml version="1.0" encoding="Shift_JIS"?>
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Test</title>
    <style type="text/css">

[disabled] {
  display: none;
}

    </style>
    <script type="application/javascript; version=1.6"><![CDATA[

if (document.implementation &&
    document.implementation.hasFeature ('XHTML'     , '2.0') &&
    document.implementation.hasFeature ('HTMLEvents', '2.0') &&
    document.implementation.hasFeature ('XPath'     , '3.0') &&
    true)

(function () {
/************************************************************************/


// bootstrapping

window.___HTMLSelectElementLinkageRegistry___ = {
    
    bootstrap : {
        handleEvent : function (event) {
            var form = document.getElementsByTagNameNS ('http://www.w3.org/1999/xhtml', 'form')[0];
            
            if (form) {
                var first = initLinkage.call (form, 'input.data1', 'input.data2', 'input.data3', 'input.data4');
                
                if (first) {
                    dispatchChangeEvent.call (first);
                }
            }
        }
    },
    
    termination : {
        handleEvent : function (event) {
            var r = ___HTMLSelectElementLinkageRegistry___;
            removeEventListener ('load'  , r.bootstrap  , false);
            removeEventListener ('unload', r.termination, false);
            delete ___HTMLSelectElementLinkageRegistry___;
        }
    }
};

window.r = ___HTMLSelectElementLinkageRegistry___;
addEventListener ('load'  , r.bootstrap  , false);
addEventListener ('unload', r.termination, false);
delete r;


////////////////////////////////////////////////////////////////////////


// extends EventTarget

function dispatchChangeEvent () {
    var event = this.ownerDocument.createEvent ('HTMLEvents');
    event.initEvent ('change', true, false);
    return this.dispatchEvent (event);
}


// extends HTMLElement

function getSingleElement (elementName) {
    var expression = 'descendant::*[ local-name() = "' + elementName + '" and namespace-uri() = "http://www.w3.org/1999/xhtml"]';
    var context    = this;
    var resolver   = this.ownerDocument.createNSResolver (this);
    var type       = XPathResult.FIRST_ORDERED_NODE_TYPE;
    
    return function (/*xpath-predicate-list*/) {
        var I = arguments.length;
        var i = 0;
        
        while (i < I) {
            var expr = expression.concat ('[', arguments[i++], ']');
            var node = document.evaluate (expr, context, resolver, type, null).singleNodeValue;
            
            if (node) return node;
        }
        
        return null;
    };
}


// extends HTMLFormElement

function initLinkage (/*name-list*/) {
    if (this.isSupported ('HTML', '2.0')) {
        Array.forEach (arguments, function (name, index, nameList) {
            var node     = this.elements[name];
            var nextName = nameList[index + 1];
            var nextNode = this.elements[nextName];
            
            if (node && nextName) {
                node.addEventListener ('change', {
                    nextNode    : nextNode,
                    handleEvent : function (event) {
                        synchronize.call (event.target, this.nextNode);
                    }
                }, false);
            }
            
            node = nextName = nextNode = null;
        }, this);
        
        return this.elements[ arguments[0] ];
    } else {
        return null;
    }
}


// extends HTMLSelectElement

function synchronize (other /*htmlSelectElement*/) {
    var label     = this.options[ this.selectedIndex ].text;
    var optgroups = other.getElementsByTagNameNS ('http://www.w3.org/1999/xhtml', 'optgroup');
    var optgroup  = enableGroup.call (optgroups, label)[0];
    var option;
    
    if (optgroup) {
        option = getSingleElement.call (optgroup, 'option')(
            'contains( " selected ", concat( " ", normalize-space( @class), " "))',
            'position() = 1'
        );
    }
    
    other.selectedIndex = option ? option.index : 0;
    dispatchChangeEvent.call (other);
}


// interface HTMLOptGroupsCollection

function enableGroup (label) {
    var result = [ ];
    
    Array.forEach (this, function (optgroup) {
        if (! (optgroup.disabled = (optgroup.label != label))) {
            result.push (optgroup);
        }
    } );
    
    return result;
};


/************************************************************************/
} )();

    ]]></script>
  </head>
  <body>
    <form action="#">
      <p>
        <select name="input.data1">
          <option>-</option>
          <option>SHARP</option>
          <option selected="selected">NEC</option>
          <option>FUJITSU</option>
        </select>
        <select name="input.data2">
          <option>-</option>
          <optgroup label="SHARP">
            <option>MZ-80K</option>
            <option class="selected">MZ-700</option>
            <option>MZ-2500</option>
          </optgroup>
          <optgroup label="NEC">
            <option>PC-6001</option>
            <option>PC-8001</option>
            <option class="selected">PC-9801</option>
            <option>PC-9821</option>
          </optgroup>
          <optgroup label="FUJITSU">
            <option>FM-8</option>
            <option>FM-7</option>
            <option>FM-11</option>
            <option>FM-77AV</option>
          </optgroup>
        </select>
        <select name="input.data3">
          <option>-</option>
          <optgroup label="MZ-700">
            <option>あいうえお</option>
            <option>かきくけこ</option>
          </optgroup>
          <optgroup label="PC-9801">
            <option>さしすせそ</option>
            <option class="selected">たちつてと</option>
          </optgroup>
          <optgroup label="FM-77AV">
            <option>なにぬねの</option>
          </optgroup>
        </select>
        <select name="input.data4">
          <option>-</option>
          <optgroup label="たちつてと">
            <option>はひふへほ</option>
            <option>まみむめも</option>
          </optgroup>
          <optgroup label="なにぬねの">
            <option>やゆよ</option>
            <option>らりるれろ</option>
            <option>わをん</option>
          </optgroup>
        </select>
      </p>
    </form>
  </body>
</html>

6   名前: 匿名 : 2007/04/15(日) 21:22  ID:jRk5Y/ta sub-Cz
IE 5.5+、Firefox 1.0+、Opera 8.0+、Safari 2.0+ 対応版。こちらは text/html でパースされることが大前提。

なお、この方法はイベント属性(onload、onchange)を上書きすることに注意。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
  <head>
    <title>Test</title>
    <script type="text/javascript">

if (document.implementation &&
    document.implementation.hasFeature ('HTML', '1.0') &&
    typeof Function.prototype      != 'undefined' &&
    typeof Function.prototype.call != 'undefined' &&
    true)

(function () {
/************************************************************************/


// bootstrapping

window.___OriginalHTMLFormElement___ = null;

window.onload = function (event) {
    var form = document.getElementsByTagName ('form')[0];
    
    if (form) {
        ___OriginalHTMLFormElement___ = form.cloneNode (true);
        var first = initLinkage.call (form, 'input.data1', 'input.data2', 'input.data3', 'input.data4');
        
        if (first) {
            dispatchChangeEvent.call (first);
        }
        
    }
};


////////////////////////////////////////////////////////////////////////


// extends EventTarget

function dispatchChangeEvent () {
    if (this.onchange) return this.onchange ();
}


// extends HTMLElement

function getSingleElement (elementName) {
    var nodes = this.getElementsByTagName (elementName);
    
    return function (/*class-or-number-list*/) {
        var i = 0;
        var I = arguments.length;
        
        while (i < I) {
            var name = arguments[i++];
            
            switch (typeof name) {
                
            case 'number' :
                return nodes[name];
                
            case 'string' :
                var j = 0;
                var J = nodes.length;
                
                while (j < J) {
                    var node = nodes[j++];
                    var expression = new RegExp (' +' + name + ' +', '');
                    
                    if (expression.test (' ' + node.className + ' ')) {
                        return node;
                    }
                }
                
            default :
                break;
            }
        }
        
        return null;
    };
}


// extends HTMLFormElement

function initLinkage (/*name-list*/) {
    var i = 0;
    var I = arguments.length;
    
    while (i < I) {
        var name     = arguments[i++];
        var node     = this.elements[name];
        var nextName = arguments[i];
        
        if (node && nextName) {
            node.onchange = function (event) {
                var name = arguments.callee.nextName;
                synchronize.call (this, this.form.elements[name]);
            };
            
            node.onchange.nextName = nextName;
        }
        
        node = nextName = null;
    }
    
    return this.elements[ arguments[0] ];
}


// extends HTMLSelectElement

function synchronize (other /*htmlSelectElement*/) {
    other = recoverNode.call (other);
    
    var label     = this.options[ this.selectedIndex ].text;
    var optgroups = other.getElementsByTagName ('optgroup');
    var optgroup  = enableGroup.call (optgroups, label)[0];
    var option;
    
    if (optgroup) {
        option = getSingleElement.call (optgroup, 'option')('selected', 0);
    }
    
    other.selectedIndex = option ? option.index : 0;
    other.selectedIndex = option ? option.index : 0;  // for fucking IE!
    dispatchChangeEvent.call (other);
}


// extends HTMLSelectElement

function recoverNode () {
    var clone, form = ___OriginalHTMLFormElement___;
    
    try {
        clone = form.elements[ this.name ].cloneNode (true);
        clone.onchange = this.onchange;
        
    } catch (error) {  // Opera 9.0
        try {
            var collection = form.elements;
            for (var i = 0, I = collection.length; i < I; i++) {
                if (collection[i].name == this.name) {
                    clone = collection[i].cloneNode (true); break;
                }
            }
            clone.onchange = this.onchange;
            
        } catch (error) {  // Safari 2.0
            document.body.appendChild (form);
            clone = form.elements[ this.name ].cloneNode (true);
            document.body.removeChild (form);
            clone.onchange = this.onchange;
        }
    }
    
    this.parentNode.replaceChild (clone, this);
    
    return clone;
}


// interface HTMLOptGroupsCollection

function enableGroup (label) {
    var result = [ ];
    var I = this.length;
    var i = 0;
    
    while (i < I) {
        var optgroup = this[i++];
        
        if (optgroup.label == label) {
            result.push (optgroup);
        } else {
            optgroup.parentNode.removeChild (optgroup); i--, I--;
        }
    }
    
    return result;
};


/************************************************************************/
} )();

    </script>
  </head>
  <body>
    <form action="#">
      <p>
        <select name="input.data1">
          <option>-</option>
          <option>SHARP</option>
          <option selected="selected">NEC</option>
          <option>FUJITSU</option>
        </select>
        <select name="input.data2">
          <option>-</option>
          <optgroup label="SHARP">
            <option>MZ-80K</option>
            <option class="selected">MZ-700</option>
            <option>MZ-2500</option>
          </optgroup>
          <optgroup label="NEC">
            <option>PC-6001</option>
            <option>PC-8001</option>
            <option class="selected">PC-9801</option>
            <option>PC-9821</option>
          </optgroup>
          <optgroup label="FUJITSU">
            <option>FM-8</option>
            <option>FM-7</option>
            <option>FM-11</option>
            <option>FM-77AV</option>
          </optgroup>
        </select>
        <select name="input.data3">
          <option>-</option>
          <optgroup label="MZ-700">
            <option>あいうえお</option>
            <option>かきくけこ</option>
          </optgroup>
          <optgroup label="PC-9801">
            <option>さしすせそ</option>
            <option class="selected">たちつてと</option>
          </optgroup>
          <optgroup label="FM-77AV">
            <option>なにぬねの</option>
          </optgroup>
        </select>
        <select name="input.data4">
          <option>-</option>
          <optgroup label="たちつてと">
            <option>はひふへほ</option>
            <option>まみむめも</option>
          </optgroup>
          <optgroup label="なにぬねの">
            <option>やゆよ</option>
            <option>らりるれろ</option>
            <option>わをん</option>
          </optgroup>
        </select>
      </p>
    </form>
  </body>
</html>

7   名前: 匿名 : 2007/04/15(日) 21:22  ID:jRk5Y/ta sub-Cz
超長文の連投ご容赦。

一応、>>4-5 では非スクリプト環境に備えて、全ての必要データを HTML 側に持たせてある。しかし、もし非スクリプト環境を考慮に入れずとも良いのであれば、以下のようなデータから select 要素を組み立てるプログラムを書けば良い。
var LinkageData = {
    'input.data1' : {
        'Maker'   : [[ 'SHARP'  , '', false ],  // text, value, selected
                     [ 'NEC'    , '', true  ],
                     [ 'FUJITSU', '', false ]]
    },
    
    'input.data2' : {
        'SHARP'   : [[ 'MZ-80K' , '', false ],
                     [ 'MZ-700' , '', true  ],
                     [ 'MZ-2500', '', false ]],
        
        'NEC'     : [[ 'PC-6001', '', false ],
                     [ 'PC-8001', '', false ],
                     [ 'PC-9801', '', true  ],
                     [ 'PC-9821', '', false ]],
        
        'FUJITSU' : [[ 'FM-8'   , '', false ],
                     [ 'FM-7'   , '', false ],
                     [ 'FM-11'  , '', false ],
                     [ 'FM-77AV', '', false ]]
   }
}

データ自体が JavaScript ネイティブだから、コード量は >>4-5 より遥かに少なくなる。巷で見つかる連動プログラムのほとんどは、こっちの方法でやっている。

構造化データを HTML で表現するか、JavaScript オブジェクトとして表現するかの違い。

8   名前: 匿名 : 2007/04/15(日) 21:22  ID:jRk5Y/ta sub-Cz
訂正。

>>7
>>4-5 → >>5-6


>>5
// extends HTMLElement

function getSingleElement (elementName) {
    var expression = 'descendant::*[ local-name() = "' + elementName + '" and namespace-uri() = "http://www.w3.org/1999/xhtml"]';
    var context    = this;
    var resolver   = this.ownerDocument.createNSResolver (this);
    var type       = XPathResult.FIRST_ORDERED_NODE_TYPE;
    
    return function (/*xpath-predicate-list*/) {
        var I = arguments.length;
        var i = 0;
        
        while (i < I) {
            var expr = expression.concat ('[', arguments[i++], ']');
            var node = context.ownerDocument.evaluate (expr, context, resolver, type, null).singleNodeValue;
            
            if (node) return node;
        }
        
        return null;
    };
}


// extends HTMLSelectElement

function synchronize (other /*htmlSelectElement*/) {
    if (this.isSupported ('HTML', '2.0')) {
        var label     = this.options[ this.selectedIndex ].text;
        var optgroups = other.getElementsByTagNameNS ('http://www.w3.org/1999/xhtml', 'optgroup');
        var optgroup  = enableGroup.call (optgroups, label)[0];
        var option;
        
        if (optgroup) {
            option = getSingleElement.call (optgroup, 'option')(
                'contains( " selected ", concat( " ", normalize-space( @class), " "))',
                'position() = 1'
            );
        }
        
        other.selectedIndex = option ? option.index : 0;
    }
    
    dispatchChangeEvent.call (other);
}


// interface HTMLOptGroupsCollection

function enableGroup (label) {
    var result = [ ];
    
    Array.forEach (this, function (optgroup) {
        if (optgroup.getAttribute ('label') == label) {
            optgroup.removeAttribute ('disabled');
            result.push (optgroup);
        } else {
            optgroup.setAttribute ('disabled', 'disabled');
        }
    } );
    
    return result;
}

一覧へ戻る