display: none; なエレメントの子が画面に出てくる Internet Explorer のバグに対処した話

やったーできたよー(^^ ω)

以下は InternetExplorer特有のバッドノウハウの話なので、18歳未満の方やバッドノウハウの描写に不快感を覚える方は退場してください。

ほんとは動くサンプルとかあったほうが良いんだろうけど、眠いので今はもう作りません。脳内処理系でなんとかしてみてください。……と思ったけど、曖昧なままエントリを書くのもどうかと思ったので作りました。

ダメな例
解決した例
とりあえずこのサンプルをIEで開いて、左から順にボタンを押してみてください。

何が問題なのか

ダメな例

たとえばこういうなんか div があって、その中にselectがあるというHTML片があります。

<div id="sticky1" style="position: absolute; width: 100px; height: 100px; top: 100px; left: 100px;">
  <select>
    <option value="1" id="ebios">いち</option>
    <option value="2"></option>
    <option value="3">さん</option>
  </select>
</div>

この div を非表示にしてみる。

<input type="button" onclick="Element.hide('sticky1');" value="Hide!"/>

でもって、内部のselectかoptionをいじる。(editボタンを押してみてね!)

$("ebios").update("エビオス嬢");

するとあらふしぎ! さっき非表示にしたはずのselectが画面上にぽつりと現れたじゃありませんか!

ここで常識的な感性を持った人なら切れ始める。

解決した。

解決した例

これが意図通りに動く例。Hide! ボタンで消したあとに、 edit! しても、ちゃんと表示されないままで変更が行われます。Show!でちゃんと表示されます。

HTMLは全く同じで、prototype.jsのElement.showとElement.updateを差し換えてあります。

内容はfix_ie_select_showing.js

// pred はDOMエレメントを引数にとる述語
// elementのparentNodeを辿り、predが真になる要素を返す。
// elementそのものもpredによってチェックされる。
Element.Methods.outer_find = function(element, pred) {
  while (!pred($(element))) {
    element = $(element).parentNode;
    if (!element || element == document.body)
      return false;
  }
  return element;
};

Element.addMethods();


//------------------------------------------------------------
// IE のバグ対策
// display: none; な要素の子要素のselectをJSで変更すると、
// 非表示だったはずのselect要素が画面上にポツンと表示されてしまう
//
Element.Methods.update_orig = Element.Methods.update;

Element.Methods.update = function(element, html) {
  Element.update_orig(element, html);
  var select = Element.outer_find(element, function(elem) {
    return elem.tagName.toUpperCase() == "SELECT";
  });
  if (!select) return element;

  var hidden = Element.outer_find(select, function(elem) {
    return !Element.visible(elem);
  });

  if (hidden) {
    select.form_hidden__ = true;
    Element.hide(select);
  }

  return element;
};

Element.Methods.show_orig = Element.Methods.show;

Element.Methods.show = function(element)
{
  Element.show_orig(element);

  var selects = $(element).getElementsByTagName("SELECT");

  for (var i = 0; i < selects.length; i++) {
    if (selects[i].form_hidden__) {
      Element.show(selects[i]);
      selects[i].form_hidden__ = false;
    }
  }
  return element;
};

Element.addMethods();
Object.extend(Element, Element.Methods);

しくみ

中身を変更するときに、いちいちElement.hideする。

制限

Element.updateとElement.showを差し替えて解決しているので、他の方法でいじるコードがあると上手く動かない。Element.updateではなく直接DOMをいじって変更を加えると、要素がポツリと表示されてしまうことになるし、Element.showではなく直接スタイルを変更してしまうと、selectが消えたままになる。ただし、prototype.js の中で静かに暮らすなら、この制限については妥協できると思います。

selectの他にもinputやtextareaでも同じようなことがおこるかもしれないけど、updateをちょっと変更すれば対処できるはずです。

もしかしたらもっと別の簡単な方法で解決できる問題なのかもしれないし、僕の他にこんなことで困ってる人がいるのかわからないけど、もし同じように困った人がいたら参考にしてみてください。

Element.show の入れ子にも対応してないのを思い出した。

考察?

こういうバッドノウハウへの対処は、いちいちアプリケーションレベルやるのはリソースの無駄なので、ライブラリ側で解決できると嬉しいですねという話でした。