アシアルブログ

アシアルの中の人が技術と想いのたけをつづるブログです

セレクトボックスのoption要素を並び替えたい!

こんちわ。松田です。
最近作っているシステムで、以下のようなお題が出されました。

問:セレクトボックスで選択した要素を↑↓ボタンで並び替えよ。ただし、CTRL選択による複数選択にも対応すること。(10点)












JavaScriptのライブラリは腐るほど出回っているのでとりあえずググろうかと思いましたが、そもそもなんてキーワードで検索したらいいのか分かりません。

「セレクトボックス 並び替え」「selectbox 要素 移動」「selectbox move」・・・ うーん。。。

日本語で検索して引っかからないと、英語ページに救いを求め、だんだんとシンプルな英単語でググっていくことになりますが、今回はなかなかいい結果が見つかりません。
そんなわけでこれは自分で実装してみることにしました。



まずは↓ボタンから作るか。ということで、クリックしたら呼び出される関数をとりあえず定義。
そして、<option>タグの要素を取ってくるところまでを作成。


<a href='javascript:;void(0);' onclick='moveDownElement()'>↓</a>

<script type="text/javascript">
    function moveDownElement() {
        var selectbox = document.getElementById('selectbox_id');
        var option_list = selectbox.getElementsByTagName('option');
    }
</script>


次に各optionを移動させるための方法を考える。
removeChild()して指定したoptionを取り出して、一つ下のoptionにappendChild()すればいいんじゃないかなーと、なんとなーく思いついたけどうまくいかない。
そっか。appendChild()だと指定した要素に子の要素として追加しちゃうから、これじゃだめなのか。
子じゃなく兄弟の位置に要素を追加するのはなんて関数だっけ・・・。なんかあったような・・・。
うーん。関数名見ればたぶん一発でわかるんだけど。


どうしても思い浮かばないので奥の手を使おう。
秘技・要素全部出し!


<script type="text/javascript">
var element = document.createElement('div');
for (var i in element) {
  document.write(element[i]);
}
</script>




お?insertBefore()ってのがある。なんかこれっぽい!
早速これを使って実装再開。

selected指定が付いているoptionをremoveChild()してinsertBefore()で挿入する。と・・・




<script type="text/javascript">
function moveDownElement() {
  var selectbox = document.getElementById('selectbox_id');
  var option_list = selectbox.getElementsByTagName('option');
  for (var i = 0; i < option_list.length; i++) {
    if (option_list[i].selected) {
      selectbox.insertBefore(selectbox.removeChild(option_list[i]), option_list[i+1]);
    }
  }
}
</script>















動いた!
けどなんか変!

そうか。[i]と[i+1]を上から順に入れ替えていくから一番上の要素が一気に下に行っちゃうのね。
よく考えたら当たり前か。やる前に気付け!
よーし、じゃあfor文を逆順にしちゃえ。



<script type="text/javascript">
function moveDownElement() {
  var selectbox = document.getElementById('selectbox_id');
  var option_list = selectbox.getElementsByTagName('option');
  for (var i = option_list.length-1; i >= 0; i--) {
    if (option_list[i].selected) {
      selectbox.insertBefore(selectbox.removeChild(option_list[i]), option_list[i+1]);
    }
  }
}
</script>

















できた!
けどまだ動きがちょっとおかしい。

optionを一つだけ動かすときはいいんだけど、複数個選択して一番下まで持ってくると、要素がくるくる入れ替わっちゃう。
なるほど。入れ替え先のoptionが一番下の要素の場合は入れ替えちゃダメなのか。
ついでに入れ替え先がselectedの場合もはじくようにしておこう。

あとはこれの↑ボタンバージョンも作って完成!



<script type="text/javascript">
function moveUpElement() {
  var selectbox = document.getElementById('selectbox_id');
  var option_list = selectbox.getElementsByTagName('option');
  for (var i = 0; i < option_list.length; i++) {
    if (option_list[i].selected) {
      if (i > 0  & & !option_list[i-1].selected) {
        selectbox.insertBefore(selectbox.removeChild(option_list[i]), option_list[i-1]);
      }
    }
  }
}

function moveDownElement() {
  var selectbox = document.getElementById('selectbox_id');
  var option_list = selectbox.getElementsByTagName('option');
  for (var i = option_list.length-1; i >= 0; i--) {
    if (option_list[i].selected) {
      if (i < option_list.length-1  & & !option_list[i+1].selected) {
        selectbox.insertBefore(selectbox.removeChild(option_list[i]), option_list[i+1]);
      }
    }
  }
}
</script>















今度こそ完成!
CTRLで複数個選択してもぐりぐり動く。わはははは。
たまにJavaScriptをいじるとすんごい楽しいですね!

-----------------------------------------------------------------------
9/21 追記

コメントにてIEで動かないとのご指摘がありましたので修正版を作成しました。
insertBeforeの環境依存かと思ったのですが、単純にremoveするoptionとinsertするoptionが逆だったみたいです。
また、IEで操作すると、↑↓ボタンで入れ替えた直後はoptionが正確に表示されない現象が起きてました。入れ替え後にマウスカーソルを乗せるとなぜか表示されます。
ためしにfocus()付けてみたらうまく動きました。
うーん・・・これはよくわからないです。

以下が修正版です。



<table border="0" style="width: 250px">
  <tr>
<td style="width: 50px">
<p><a href='javascript:;void(0);' onclick='moveUpElement()'>↑</a></p>
<p><a href='javascript:;void(0);' onclick='moveDownElement()'>↓</a></p>
</td>
<td>
<select id="selectbox_id" multiple="multiple" size="10" style="width:150px">
  <option value=1>ああああああ</option>
  <option value=2>いいいいいい</option>
  <option value=3>うううううう</option>
  <option value=4>ええええええ</option>
  <option value=5>おおおおおお</option>
  <option value=6>かかかかかか</option>
  <option value=7>きききききき</option>
  <option value=8>くくくくくく</option>
  <option value=9>けけけけけけ</option>
  <option value=0>ここここここ</option>
</select>
</td>
</tr>
</table>

<script type="text/javascript">
function moveUpElement() {
  var selectbox = document.getElementById('selectbox_id');
  var option_list = selectbox.getElementsByTagName('option');
  for (var i = 0; i < option_list.length; i++) {
    if (option_list[i].selected) {
      if (i > 0  & & !option_list[i-1].selected) {
        selectbox.insertBefore(selectbox.removeChild(option_list[i]), option_list[i-1]);
        selectbox.focus();
      }
    }
  }
}

function moveDownElement() {
  var selectbox = document.getElementById('selectbox_id');
  var option_list = selectbox.getElementsByTagName('option');
  for (var i = option_list.length-1; i >= 0; i--) {
    if (option_list[i].selected) {
      if (i < option_list.length-1  & & !option_list[i+1].selected) {
        selectbox.insertBefore(selectbox.removeChild(option_list[i+1]), option_list[i]);
        selectbox.focus();
      }
    }
  }
}
</script>
















-----------------------------------------------------------------------
9/25 さらに追記

selectbox.focus()を入れてからやけに表示がチラチラしてたのでソースコードを見直したら、forループの中でfocus()をやってました。これじゃ重くなるわけだ。。
と言うわけでselectbox.focus()の位置をforループ後に書き換えました。

また、はてぶのコメントで「insertBeforeするときはremoveChildしなくてもいい」と言う情報がありましたので試してみたところ確かに動いてます。
「newChild が既に樹の中にある場合には、まずそれが取り除かれる。 」んだそうです。知らなかった・・・。


selectbox.insertBefore(option_list[i+1], option_list[i]);

最終的にはこれでいけるようですね。
情報ありがとうございます!

そんなわけで今度こそ完成版です!


<script type="text/javascript">
function moveUpElement() {
  var selectbox = document.getElementById('selectbox_id');
  var option_list = selectbox.getElementsByTagName('option');
  for (var i = 0; i < option_list.length; i++) {
    if (option_list[i].selected) {
      if (i > 0  & & !option_list[i-1].selected) {
        selectbox.insertBefore(option_list[i], option_list[i-1]);
      }
    }
  }
  selectbox.focus();
}

function moveDownElement() {
  var selectbox = document.getElementById('selectbox_id');
  var option_list = selectbox.getElementsByTagName('option');
  for (var i = option_list.length-1; i >= 0; i--) {
    if (option_list[i].selected) {
      if (i < option_list.length-1  & & !option_list[i+1].selected) {
        selectbox.insertBefore(option_list[i+1], option_list[i]);
      }
    }
  }
  selectbox.focus();
}
</script>















めっちゃ長い記事になっちゃった。