Flash の z-index を Javascript から有効にする
趣旨
Flash は、ただのインラインレベル置換要素であるにもかかわらず、デフォルトでは CSS の z-index に関する決まりごとに従わない。z-index とは、画面の x軸・y軸の両方に直交する軸で、要素が画面の手前におかれているのかそれとも奥かを指示する CSS プロパティである。たとえば、position:absolute で z-index を指定した DIV 要素を z-index の指定のない Flash と重ねると、本来ならば、DIV は Flash の手前に表示されなければならないのに、実際には Flash のほうが手前に来てしまう。
では打つ手はないのか、というと実は wmode という Flash のプロパティがあり、これを "transparent" にしてやればよい。たとえばつぎのような感じだ。
<!-- EMBED を使うとき --> <embed src="foobar.swf" wmode="transparent" /> <!-- OBJECT を使うとき --> <object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"> <param name="movie" value="foobar.swf"/> <param name="wmode" value="transparent"/> </object>
これで z-index に関して普通の HTML 要素のように振舞うようになるようだ。(CSS21:9.9.1)
今回の話は、この wmode プロパティを Javascript で動的に設定するにはどうするか、というお話である。簡単に思われるかもしれないが、これがなかなか骨なのである。
Javascript の罠
次のサンプルを考えてみる。red_clock.swf と blue_clock.swf という Flash があると想定する。(実際のところサンプルには、Flashbucks を使わせていただいた。ここで感謝を表明したい)
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html> <head> <title>flash test</title> <style type="text/css"> #div1 { background-color: blue; position:absolute; width:100px; height:100px; left:50px; top:10px; z-index: 30; } #blue_clock { z-index: 20; position:absolute; width:100px; height:100px; left:50px; top:10px; } #div2 { background-color: red; position:absolute; width:100px; height:100px; left:200px; top:10px; z-index: 30; } #red_clock { position:absolute; width:100px; height:100px; left:200px; top:10px; z-index: 20; } #link1 { position:absolute; width:100px; height:30px; left:350px; top:10px; } #link2 { position:absolute; width:100px; height:30px; left:350px; top:50px; } </style> <script type="text/javascript"> function bring_div1_to_front() { var flash = document.getElementById("blue_clock"); var param = document.createElement("param"); param.setAttribute("name", "wmode"); param.setAttribute("value", "transparent"); flash.appendChild(param); } function bring_div2_to_front() { var elem = document.getElementById("red_clock"); elem.setAttribute("wmode", "transparent"); } </script> </head> <body> <div id="div1"></div> <object id="blue_clock" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"> <param name="movie" value="blue_clock.swf" /> </object> <div id="div2"></div> <embed id="red_clock" src="red_clock.swf" /> <a href="#" id="link1" onclick="bring_div1_to_front();">bring_div1_to_front</a> <a href="#" id="link2" onclick="bring_div2_to_front();">bring_div2_to_front</a> </body> </html>
上のサンプルをブラウザで見てみると、2つの時計の Flash の右側に bring_div1_to_front, bring_div2_to_front というリンクがある。これらをクリックすると同名の関数が実行されて、それぞれ、OBJECT, EMBED 各要素において、wmode プロパティに "transparent" という値を設定する。スタイルで div1, div2 のほうが Flash より z-index の値が大きいので、本来ならば div が Flash の手前に見えるはずだが、何も起こらない。Flash は詳しくないので、どうしてこうなってしまうのかわからない。強制再描画みたいな手段があるのかもしれないが・・・。
対策
はっきり言うとまっとうな対策はむずかしい。私がやったことはかなり無理やりなことだった。いろいろ試してみたが、結局 EMBED 要素を一度、文字列にし、文字列として wmode="transparent" を追加し、ついでそれをしかるべき要素の innerHTML に突っ込むことにより、ふたたび EMBED 要素にするという荒業しか思いつかなかった。 それゆえ、OBJECT 要素のみで、Flash を組み込んでいる HTML 文書には対応できない。以下にコードを掲げるので、興味があれば読んでほしいが、特に OBJECT 要素を EMBED 要素に置き換えてしまうあたり、ややヤケクソ感さえ漂う。とりあえずは IE でも Firefox でも動く。(試していないが Safari でも大丈夫なはずだ) ないよりはましだろうということでご勘弁願いたい。もしこれよりうまいやり方をご存じな方はぜひ教えてください。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html> <head> <title>flash test</title> <style type="text/css"> #div1 { background-color: blue; position:absolute; width:50px; height:50px; left:100px; top:30px; z-index: 30; } .blue_clock { /*position:absolute;*/ z-index: 20; width:100px; height:100px; left:20px; top:10px; z-index: 20; } #div2 { background-color: red; position:absolute; width:50px; height:50px; left:280px; top:30px; z-index: 30; } .red_clock { position:absolute; width:100px; height:100px; left:200px; top:10px; z-index: 20; } #link1 { position:absolute; width:100px; height:30px; left:350px; top:10px; } #link2 { position:absolute; width:100px; height:30px; left:350px; top:50px; } </style> <script type="text/javascript"> function _getComputedStyle(element) { if(window.getComputedStyle) { return document.defaultView.getComputedStyle(element, null); } else if(element.currentStyle) { return element.currentStyle; } }; function copyDisplayProperties(src, dest) { var ss = _getComputedStyle(src); var ds = dest.style; var props = "display,position,float,left,top,width,height," + "marginTop,marginRight,marginBottom,marginLeft," + "borderTopWidth,borderRightWidht,borderBottomWidth,borderLeftWidth," + "paddingTop,paddingRight,paddingBottom,paddingLeft"; var prop_list = props.split(","); for(var i = 0, len = prop_list.length; i < len; i++) { var prop = prop_list[i]; ds[prop] = ss[prop]; } } function replaceOneObjectWithEmbed(object) { var html = object.innerHTML; var res = html.match(/(<embed.+?)>/im); if(!res) { return; } var embed_html_open_end = res[1]; var embed_html = embed_html_open_end + ' wmode="transparent">'; // 直接 object.innerHTML = embed_html としないのは、IE でエラーになるからである。 var span = document.createElement("span"); span.innerHTML = embed_html; copyDisplayProperties(object, span); var parent = object.parentNode; parent.replaceChild(span, object); } function bringAllObjectsBehind() { var nodes = document.getElementsByTagName("object"); for(var i = 0, len = nodes.length; i < len; i++) { var node = nodes[i]; replaceOneObjectWithEmbed(node); } } function makeEmbedWmodeTransparent(embed) { var wmode = embed.getAttribute("wmode"); // Do nothing in case that wmode has already been set to "transparent" if(wmode && wmode.match(/^transparent$/i)) { return; } var parent = embed.parentNode; var span = document.createElement("span"); embed = parent.replaceChild(span, embed); span.appendChild(embed); var html = span.innerHTML; var html2 = html.replace(/<EMBED /i, '<embed wmode="transparent" '); span.innerHTML = html2; } function bringAllEmbedsBehind() { var nodes = document.getElementsByTagName("embed"); for(var i = 0, len = nodes.length; i < len; i++) { var embed = nodes[i]; makeEmbedWmodeTransparent(embed); } } function bringAllFlashesBehind() { bringAllObjectsBehind(); bringAllEmbedsBehind(); } </script> </head> <body> <div id="div1"></div> abc <object class="blue_clock" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"> <param name="movie" value="blue_clock.swf" /> <embed class="blue_clock" src="blue_clock.swf" style="left:0px; top:0px;" /> </object> def <div id="div2"></div> <embed class="red_clock" src="red_clock.swf" /> <a href="#" id="link1" onclick="bringAllFlashesBehind();">bringAllFlashesBehind</a> </body> </html>