対象ブラウザは、InternetExplorer 6.0、Firefox 1.5、Opera9。

はじめに {#first}

<div id="content">
    <ul class="navi">
  • Home
  • Diary
  • Text
  • Link

このようなHTMLが存在し、JavaScriptで全てのli要素に下線を引きたいとする。

prototype.jsを導入しているなら$( “content” ).getElementsByTagName( “li” )でdiv#content内の全てのli要素を取得して、forループで回してstyleにアクセスして・・・とすれば確かに目的は達せられるが対象の要素が増えれば増えるほど処理に時間がかかるし、コードが複雑化していく。

だが、ある程度HTML+CSSでwebページを作ったことがあるならば、このようなスタイルシートを用意するだけで色を変えることだけは解決するのに気づくだろう。

#content .navi li {
    border-bottom : 1px solid #000;
}

このほうがより効率的でスマートだ。本文書では、これをJavaScriptでどのようにして動的に行うかを記述していく。

用語について {#term}

#content .navi li部分が「セレクタ」
border-bottom部分が「プロパティ」
これらひっくるめて「ルール」
ルールをまとめたものを「スタイルシート」
と呼ぶことにしている。

camelize {#camelize}

プロパティ指定の互換を取るためにcamelizeとしてハイフン+小文字アルファベットを大文字アルファベットに直す関数を定義しておく。deCamelizeはその逆。

String.prototype.camelize = function( ) {
    return this.replace( /-([a-z])/g,
        function( $0, $1 ) { return $1.toUpperCase( ) } );
}
String.prototype.deCamelize = function( ) {
    return this.replace( /[A-Z]/g,
        function( $0 ) { return "-" + $0.toLowerCase( ) } );
}

ルールを追加する {#addRule}

//戻り値 : 挿入したルールの位置を表すインデックス(Number)
function addRule( selector, property, sheetindex, ruleindex ) {
    if( sheetindex == undefined ) sheetindex = 0;
    var sheet = document.styleSheets[ sheetindex ];

    if( sheet.addRule ) { //IE
        if( ruleindex == undefined ) ruleindex = sheet.rules.length;
        sheet.addRule( selector, "{" + property + "}", ruleindex );
        return ruleindex;
    }
    else if( sheet.insertRule ) { //Mozilla
        if( ruleindex == undefined ) ruleindex = sheet.cssRules.length;
        return sheet.insertRule( selector + "{" + property + "}", ruleindex );
    }

    return null;
}

addRule及びinsertRuleは最後尾にルールを追加していく。ルールは基本的にセレクタが同じなら後から指定したものが有効になるので、どんどん追加していって構わない。

addRule( "#content .navi li", "display : none" );

これで対象すべてを非表示にすることができる。

addRule( "#content .navi li", "display : block" );

あとから追加したものが有効になるので、こうすれば元に戻すことができる。

ルールを取得する {#getStyleValue}

//戻り値 : プロパティの値(String)
function getStyleValue( selector, property, sheetindex ) {
    selector = selector.toLowerCase( )
    if( sheetindex == undefined ) sheetindex = 0;
    if( property.indexOf( "-" ) != -1 ) property = property.camelize( );
    var rules = document.styleSheets[ sheetindex ].rules //IE
        || document.styleSheets[ sheetindex ].cssRules; //Mozilla

    for( var i = rules.length - 1; i >= 0; i-- ) {
        var rule = rules[i];
        if( rule.selectorText.toLowerCase( ) != selector
        || rule.style[ property ] == "" ) continue;
        return rule.style[ property ];
    }

    return null;
}

こうすることで、現在スタイルシートに適用されているルールの値を直接取得することができる。

色を取得する場合はブラウザ毎に挙動が異なるので注意する。IEは指定したまま取得できるが、Operaは色を勝手に#16進6桁に展開。Firefoxに至ってはRGBフォーマットにしてしまう。

ブラウザ background : #123 background : blue
IE #123 blue
Opera #112233 #0000ff
Firefox rgb(17, 34, 51) blue

またFirefoxは短縮形で取得しようとすると、設定していないプロパティにデフォルト値が入った状態で返ってくるので注意する。

getStyleValue( "div.hoge", "background" )
//Mozilla : rgb(17, 34, 51) none repeat scroll 0% 0%

ルールを削除する {#deleteRule}

function deleteRule( index, sheetindex ) {
    if( sheetindex == undefined ) sheetindex = 0;
    document.styleSheets[ sheetindex ].deleteRule( index );
}

全てのルールにはインデックスが振られており、それを指定することでルールから消すことができる。

ただ、同じセレクタで上書きするのとほとんど変わらないので、あまり必要にならないだろう。

スタイルシートを無効にする {#disableSheet}

function disableSheet( sheetindex ) {
    document.styleSheets[ sheetindex ].disabled = true
}

これだけ。インライン指定したものは有効なままなので注意。

要素に適用されているスタイルを取得する {#getActiveStyle}

div#hoge { margin-top : 10px }

このようにスタイルシートに記述して、該当要素を取得してstyle.marginTopの値を見ても中身は空である。今現在画面に適用されているスタイルを取得したい場合はcurrentStyleかgetComputedStyleを使って取得する。

//戻り値 : プロパティの値(String)
function getActiveStyle( element, property, pseudo ) {
    if( element.currentStyle ) { //IE or Opera
        if( property.indexOf( "-" ) != -1 ) property = property.camelize( );
        return element.currentStyle[ property ];
    }
    else if( getComputedStyle ) { //Mozilla or Opera
        if( property.indexOf( "-" ) == -1 ) property = property.deCamelize( );
        return getComputedStyle( element, pseudo ).getPropertyValue( property );
    }

    return "";
}

getActiveStyle( $( "hoge" ), "marginTop" );
getActiveStyle( $( "hoge" ), "margin-top" );
getActiveStyle( $( "hoge" ), "margin-top", ":first-line" );

getComputedStyleの第二引数は擬似要素を取得したいときに使う。:first-lineとか:visitedとか。IEはcurrentStyleしか使えないので擬似要素の取得が不可能となっている。

getComputedStyleとcurrentStyleについて {#getComputedStyle}

getComputedStyleは現在画面に適用している値をそのまま返すので、emだろうがptだろうが関係なくpxに変換されてしまう。

仮にemで指定した場合、ブラウザのフォント設定によって結果が変わってしまう。ブラウザ側で18pxと設定してCSSに1emと記述した場合、1em=1文字の大きさ=設定された18pxとなり、18pxが返ってくる。

Firefox限定になるが、ブラウザ側でズームしてフォントを大きくした場合でもしっかり対応してくれるので、使いようによっては便利かもしれない。上記の例なら2回フォントを大きくしてから取得すれば、しっかり50%増しの27pxが返ってくる。試していないが、モニタ解像度を96dpiから 72dpiに変えたりすれば、pt指定の値も変化するものと思われる。

currentStyleは、純粋にスタイルの優先順位等を考慮した値をそのまま返してくれるので、emで指定していたらemが、ptならptが返ってくる。

なお、OperaはgetComputedStyleとcurrentStyleの両方が利用できるので、状況に応じて使い分けるといいだろう。

#hoge { margin-top : 1em }

getActiveStyle( $( "hoge" ), "margin-top" );
//IE : 1em
//Firefox : 18px
//Opera(currentStyle) : 1em
//Opera(getPropertyValue) : 18px

全称セレクタに関する注意 {#universal}

IEにはセレクタに全称セレクタが入っていると、登録時に削除される仕様(不具合?)が存在する。

ul * { line-height : 150% }

getStyleValue( "ul *", "line-height" );

このようにしてもnullが返ってきてしまう。document.styleSheets[0].rulesを参照しても、上記の場合はselectorTextがulのみになってしまう。

じゃあアスタリスクを除いたulだけ指定して取得すればいいじゃん、と思うが

ul { margin : 1em }
ul * { margin : 0.5em }

このように指定されていたら、どちらが本来取得したい「ulの子要素全てのルール」か判断することができなくなってしまう。

セレクタが全称セレクタのみだった場合は、セレクタを空文字列で指定することで取得することができる。

* {
    margin : 0;
    padding : 0;
}

getStyleValue( "", "margin" );

タグの間に挟んだ場合は、その部分だけを削除して指定すればよい。

ul * span { line-height : 150% }

getStyleValue( "ul  span", "line-height" );

スペースを二つ空けるのがポイント。一つではダメ。

この不具合はIEのみで、FirefoxとOperaは関係ない。

セレクタの短縮形に関する注意 {#abbreviation}

div { margin : 20px }

短縮形を用いることで、このようにmarginを四方向一気に設定することができるが、Firefoxのみ、getPropertyValueから短縮形を直接指定すると一部のプロパティが空文字列を返してしまう。

margin : ""
marginTop : "20px"
marginRight : "20px"
marginBottom : "20px"
marginLeft : "20px"

内部ではこのようになってしまっているので、一方向ずつ取得しなければならない。Operaの場合は短縮形でも問題無く取得することができる。

セレクタのグループ化に関する注意 {#grouping}

ul li, span { line-height : 150% }

カンマで区切ってグループ化する場合、IEは内部で分解され、FirefoxとOperaでは分解されずに登録される。

//IE
getStyleValue( "ul li", "line-height" );
getStyleValue( "span", "line-height" );

//Firefox,Opera
getStyleValue( "ul li, span", "line-height" );

このように、ブラウザで処理を分けなくてはならなくなる。

対処法としては、グループ化を行わないのが最も簡単。CSSの記述方法にプロパティ別整理法というものが存在するので、試してみるといいかもしれない。

ブラウザの実装による比較 {#comparison}

IE : − セレクタ毎に分割登録される : × 擬似要素(:visited等)が取得できない : × 全称セレクタが削除されて登録される

Firefox : − セレクタ毎に分割登録されない : △ ブラウザに忠実なpxサイズしか取得できない : △ カラーコードが展開される : △ プロパティの自動補完 : × 短縮形セレクタで一部取得できない

Opera : − セレクタ毎に分割登録されない : △ カラーコードが展開される : ○ IE/Firefox両方の方法で取得できる : × 9以前ではスタイルシートにアクセスできない

カラーコードは#112233のように6桁で記述し、セレクタのグルーピングを止め、短縮形による取得を止めれば、3ブラウザでほぼ共通の動作が期待できるだろう。

Opera8以下について {#opera8}

Operaはバージョンが9になってからDOM2CSS、いわゆるinsertRule等を実装するようになったので、それ以前のバージョンではDOM2CSSを利用することができない。

しかし、多少制限はあるものの、動的にルールを追加することは一応可能なので、その方法を記述する。

なお、getComputedStyleが使えるので、現在適用されているスタイルの取得は可能。

addStyle {#addStyle}

dataスキームを利用して、cssファイルそのものが存在するかのように見せる。

function addStyle( selector, property ) {
    var link = document.createElement( "link" );
    link.rel = "stylesheet";
    link.href = "data:text/css;charset=utf-8;base64,"
        + btoa( selector + "{" + property + "}" );
    link.type = "text/css";
    document.getElementsByTagName( "head" )[ 0 ].appendChild( link );
}

btoaには高度なJavaScript技集のBase64 encodeを定義する必要がある。

参考 {#reference}

この文章について {#about}

日記のほうで3回に渡って記述した「JavaScriptでCSSを弄る際のメモ その1その2その3」と「Operaで動的にグローバルなスタイルを変更する」をOpera9リリースに合わせて修正し、まとめたものです。

もし記述に間違い等を見つけられたなら、教えてくれると幸いです。その際はトップページ最下部のフォームから送信してください。

2006/8/25 涼崎聡