セレクトボックス間でのoptionタグの移動



0   名前: hige : 2007/09/19(水) 13:49  ID:e87Je0k4 sub-99
宜しくお願い致します。
以下のようなソースで、2つのセレクトボックス間で「→」「←」ボタンをクリックすると、
optionタグが移動するサンプルを作成しております。
左から右はうまくいくのですが、右から左が何故かうまくいきません。
アドバイス頂けると幸いで御座います。



<html>
<head>
<script type="text/javascript">
function pushRight(){
var leftNode = document.getElementById('left');
var rightNode = document.getElementById('right');

var textNode;
var optionNode;

var tempAry = new Array();

// 右へ追加
for (var i = 0; i < leftNode.options.length; i++) {
if (leftNode.options[i].selected == true) {
textNode = null;
textNode = document.createTextNode(leftNode.options[i].text);

optionNode = document.createElement('option');
optionNode.appendChild(textNode);
optionNode.id = leftNode.options[i].id;
optionNode.value = leftNode.options[i].value;
rightNode.appendChild(optionNode);

tempAry.push(leftNode.options[i].id);
}
}

// 左を削除
tempAry = tempAry.reverse();
for (var i = 0; i < tempAry.length; i++) {
leftNode.removeChild(document.getElementById(tempAry[i]));
}
}

function pushLeft(){
var leftNode = document.getElementById('left');
var rightNode = document.getElementById('right');

var textNode;
var optionNode;

var tempAry = new Array();

// 右へ追加
for (var i = 0; i < rightNode.options.length; i++) {
if (rightNode.options[i].selected == true) {
textNode = null;
textNode = document.createTextNode(rightNode.options[i].text);

optionNode = document.createElement('option');
optionNode.appendChild(textNode);
optionNode.id = rightNode.options[i].id;
optionNode.value = rightNode.options[i].value;
leftNode.appendChild(optionNode);

tempAry.push(rightNode.options[i].id);
}
}

// 左を削除
tempAry = tempAry.reverse();
for (var i = 0; i < tempAry.length; i++) {
rightNode.removeChild(document.getElementById(tempAry[i]));
}
}
</script>
</head>
<body>

<select id="left" size="6" multiple>
<option value="1" id="1">hoge1</option>
<option value="2" id="2">hoge2</option>
<option value="3" id="3">hoge3</option>
<option value="4" id="4">hoge4</option>
<option value="5" id="5">hoge5</option>
<option value="6" id="6">hoge6</option>
</select>

<select id="right" size="6" multiple>
</select>

<input type="button" value="→" onclick="pushRight();">
<input type="button" value="←" onclick="pushLeft();">
</body>
</html>



エラーメッセージ:
uncaught exception
"Node was not found"


ブラウザ:
Fireforx2.0.0.6

1   名前: 匿名 : 2007/09/19(水) 13:49  ID:NuhnDyNX sub-Cz
エラーの行数と例外の種類も忘れずに。
Error: uncaught exception: [Exception... "Node was not found"
code: "8" nsresult: "0x80530008 (NS_ERROR_DOM_NOT_FOUND_ERR)"
location: "************** Line: 64"]

エラーの直接の発生行は以下。
rightNode.removeChild(document.getElementById(tempAry[i]));

エラーの種類は「NS_ERROR_DOM_NOT_FOUND_ERR」。ここで、「NS_ERROR_DOM_」は Gecko 特有の接頭辞だから無視するとして、「NOT_FOUND_ERR」が removeChild() で発生する条件を調べる。

http://www2u.biglobe.ne.jp/~oz-07ams/prog/dom-ref/Core/Node.html#Node-removeChild
nodeObject.removeChild( oldChild )
....
NOT_FOUND_ERR / DOMException
  oldChild がこのノードの子でない場合に発生。

つまり、document.getElementById(tempAry[i]) が rightNode の子ではない、と言われている。



なぜか。根本的な原因は以下の部分。
optionNode = document.createElement('option');
optionNode.appendChild(textNode);
optionNode.id = rightNode.options[i].id;
optionNode.value = rightNode.options[i].value;
leftNode.appendChild(optionNode);

同一ドキュメント内で ID 重複が発生している。この場合の getElementById() の動作は実装依存だが、多くの実装では最初に見つけたノードを返す。従って、上述の document.getElementById(tempAry[i]) は rightNode の子ではなく、文書内順序で前の方にある leftNode の子を返す可能性が高い。

そもそも、マトモな DOM 実装なら leftNode.appendChild (rightNode.options[i]) でノードを移動できる。いちいちクローンを(しかも cloneNode() も使わず)作る必要なんてないわけだ。


ついでに、ID 型の属性は数字で始めることはできない。それ以外にも、>>0 のソースでは省略不可能な要素が省略されているし、逆に省略可能な要素がご丁寧に書かれているし、もう少し HTML の文法に気を付けた方が良いと思う。DOM-HTML は、DTD を知らないとドツボにはまるよ(特にデフォルト内容の有無)。

2   名前: 匿名 : 2007/09/19(水) 13:49  ID:NuhnDyNX sub-Cz
>>1
x 逆に省略可能な要素が
o 逆に省略可能なタグが

3   名前: hige : 2007/09/19(水) 13:49  ID:e87Je0k4 sub-99
匿名様

ご回答ありがとう御座います。

エラーの種類の特定、DOMの考え方等
とても勉強になりました。
ご指摘頂きました箇所を修正し、
想定機能を実現することができました。
ありがとう御座いました。

以下に修正したソースを記述させて頂きます。



<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=euc-jp" />
<script type="text/javascript">
function pushRight(){
var leftNode = document.getElementById('left');
var rightNode = document.getElementById('right');

var textNode;
var optionNode;

var tempAry = new Array();

for (var i = 0; i < leftNode.options.length; i++) {
if (leftNode.options[i].selected == true) {
tempAry.push(leftNode.options[i].id);
}
}

for (var i = 0; i < tempAry.length; i++) {
rightNode.appendChild(document.getElementById(tempAry[i]));
}
}

function pushLeft(){
var leftNode = document.getElementById('left');
var rightNode = document.getElementById('right');

var textNode;
var optionNode;

var tempAry = new Array();

for (var i = 0; i < rightNode.options.length; i++) {
if (rightNode.options[i].selected == true) {
tempAry.push(rightNode.options[i].id);
}
}

for (var i = 0; i < tempAry.length; i++) {
leftNode.appendChild(document.getElementById(tempAry[i]));
}
}
</script>
</head>
<body>

<select id="left" size="6" multiple>
<option value="1" id="aa">hoge1</option>
<option value="2" id="bb">hoge2</option>
<option value="3" id="cc">hoge3</option>
<option value="4" id="dd">hoge4</option>
<option value="5" id="ee">hoge5</option>
<option value="6" id="ff">hoge6</option>
</select>

<select id="right" size="6" multiple>
</select>

<input type="button" value="→" onclick="pushRight();">
<input type="button" value="←" onclick="pushLeft();">
</body>
</html>



> それ以外にも、>>0 のソースでは省略不可能な要素が省略されているし、逆に省略可能な要素がご丁寧に書かれているし、もう少し HTML の文法に気を付けた方が良いと思う。
上記につきまして、お時間のあるときにでも
具体的にご指摘頂ければ幸いで御座います。
何卒宜しくお願い致します。

4   名前: 匿名 : 2007/09/19(水) 13:49  ID:/.TkSTfZ sub-Cz
まあテキトーに読み流してほしいが、>>3 に関して言えば、

・空要素型(meta、input)に終了タグがあったりなかったり。この時点で、>>3 は SGML(HTML)構文としても、XML(XHTML)構文としても破綻。
・さらに、script 要素内に「<」があるため、ここでも整形式が破綻。従って、>>3 が XHTML であることはありえない。
・ごく寛容に HTML として考えるなら script 要素内に「<」があっても良いが、SGML の規則上、ドキュメント冒頭に DOCTYPE 宣言がなければならない。

このように基本構文が破綻していると、SGML/XML 用の各種ツールが使えず、相互運用性に多大な支障をきたすので、こういうレベルでのミスは絶対に避けよう。



さて、上記の構文エラーを修正し、HTML 4.01 を宣言したとする。この場合、

・head 要素型は、子として必ず 1 つの title 要素型を持たねばならない。
・HTML 4.01 は、原則として body 要素型の直下にインライン要素型(この場合 select/input 要素)を置くことはできない。ただし、過去バージョンの HTML との整合性のために、HTML 4.01 Transitional を宣言した場合は、この限りではない。

・細かい点だが、onclick 属性など組み込みイベントを使う際は、Content-Script-Type でデフォルト言語の宣言が義務付けられている。

HTML 4.01 では、html、head、body 要素型の開始タグと終了タグ、option 要素型の終了タグは条件付きで省略可とされている。だから、仮に >>3 を HTML 4.01 Transitional と考えるなら、
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<meta http-equiv="Content-Script-Type" content="text/javascript">
<title>TEST</title>
<script type="text/javascript">...</script>
<select ...><option...>hoge1<option...>hoge2<option...>hoge3</select>
<select ...></select>
<input ...>
<input ...>

これが最小の形になる。無論、処理系がきちんと解釈できるかは別問題だから、タグ省略しないに越したことはないが。

ここで言いたいのは、タグで明示されずとも、要素が暗黙に存在しうる場合もあるということだ。このことは、DOM ツリーを操作する上で極めて大事なポイントになる。タグは移動しない。要素が移動するんだ。



先に述べたが、

・head 要素型は、子として必ず 1 つの title 要素型を持つ。

従って、DOM-HTML パーサは、たとえドキュメントに書かれていようといまいと必ず title 要素型ノードを作成する(title 要素型のタグは省略不可だから、本当は書かれてないと駄目だが)。

また、HTML 4.01 と XHTML 1.0 では、要素型の内容モデルが異なることもある。

・HTML 4.01 では、table 要素型の直下には(タグで明示されていなくとも)必ず 1 つ以上の tbody 要素型が存在するが、XHTML 1.0 では tbody 要素型がなくても良い。

従って、DOM-HTML パーサが tbody 要素型ノードを暗黙に作成しうることに注意しなければならない。

・HTML 4.01 では、fieldset 要素型の直下に legend 要素型が必ず 1 つだけ含まれている(かつ、タグで明示しなければならない)が、XHTML 1.0 では legend 要素型がなくても良い。

従って、DOM-HTML パーサが legend 要素型ノードを暗黙に作成しうることに注意しなければならない。

このように、DOM 操作するときは、自分が何のルールに基づくドキュメントを扱っているのかを念頭に置かないと、「何で?」という事態に陥りやすい。

5   名前: 匿名 : 2007/09/19(水) 13:49  ID:e87Je0k4 sub-99
匿名様

ご丁寧なご回答ありがとう御座います。
非常に勉強になりました。
これからはHTMLの文法も意識してプログラミングしていきます。
ありがとう御座いました。

一覧へ戻る