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>