これでできる! クロスブラウザJavaScript入門

第10回JavaScriptとCSS

こんにちは、太田です。前々回前回はHTMLの操作について解説しました。今回は、CSSの操作を中心に解説していきます。

CSSとJavaScript

JavaScriptからCSSを扱うとは、JavaScriptから要素に適用されているスタイルを変更して見た目を変化させる、ということを意味します。その具体的な方法にはいくつかの種類があります。

  1. styleプロパティの操作
  2. class名の操作
  3. CSS自体の操作

では、styleプロパティの操作から順番に見ていきます。

styleプロパティの操作

要素のstyleプロパティを直接操作する方法は、その要素だけに影響するので1回あたりの処理コストは低く済むというメリットはあります。しかし、複数のプロパティの操作に加え、多くの要素のスタイルを変更する場合には、スタイルを変更するたびに描画への影響の計算が行われる(この計算をreflowと呼びます)ので、ボトルネックになりかねないという問題もあります。

styleプロパティの操作
var div = document.getElementById('css-sample1');
div.style.fontSize = '20px';
div.style.color = 'white';
div.style.backgroundColor = '#444';
div.style.width = '100px';
div.style.padding = '10px';
サンプル

なお、ここで次のようにstyleプロパティを変数に入れることで、少しだけコードを短くし、処理時間もほんの僅かだけ速くすることもできます。

styleプロパティの操作#2
var div = document.getElementById('css-sample1');
var style = div.style;
style.fontSize = '20px';
style.color = 'white';
style.backgroundColor = '#444';
style.width = '100px';
style.padding = '10px';

なお、CSSではfont-sizeですが、上記のようにJavaScriptではfontSizeと表記します。これはJavaScriptではハイフンがマイナスとして解釈されてしまうため、プロパティ名に使えないからです。もっとも、objest['xx-プロパティ'] の形であればハイフンがマイナスとして解釈されないので、⁠ハイフンに限らず)自由にプロパティ名を作ることができます。

なお、この違いはハイフンの次の文字を大文字にするという簡単なルールなので変換が可能です。この処理はCamelize、もしくはCapitalizeと呼ばれることが多いようです。

Camel Caseへの変換#1
function camelize(prop){
  return prop.replace(/-([a-z])/g, function(m, m1){
    return m1.toUpperCase();
  });
}
camelize('font-size'); // fontSize
camelize('-moz-border-radius'); // MozBorderRadius

正規表現でハイフンの次の文字をキャプチャしておき、その文字をtoUpperCaseで大文字に変換しています。もしくは次のようにcharAtで2文字目を大文字化してもよいでしょう。

Camel Caseへの変換#2
function camelize(prop){
  return prop.replace(/-[a-z]/g, function(m){
    return m.charAt(1).toUpperCase();
  });
}
camelize('font-size'); // fontSize
camelize('-moz-border-radius'); // MozBorderRadius

-moz-border-radiusのように、先頭にベンダー接頭辞が付く場合も同様です。なお、ベンダー接頭辞付きのスタイルはベンダー接頭辞なしのスタイルをサポートしたときに使えなくなる場合があるので、なるべくベンダー接頭辞ありのスタイルとベンダー接頭辞なしのスタイルの両方を指定するようにしましょう。

ベンダー接頭辞付きのスタイルのサポートが切られたケースとして、Mozillaはopacityとそのベンダー接頭辞版である-moz-opacityの両方をFirefox0.9の頃からFirefox3.0までサポートし続けていましたが、Firefox 3.5からは-moz-opacityをサポートしなくなっています。Mozillaは今後もベンダー接頭辞は積極的にサポートを打ち切りしていく方針を示しています。

あくまで、ベンダー接頭辞付きのスタイルは仕様が確定するまでの間に仕方なく試用されるものであり、将来的には無効になるものです。そのことは忘れないようにしましょう。

styleプロパティが存在するか確認する方法

上記でFirefox 3.5が-moz-opacityをサポートしなくなったと書きましたが、そういったサポート状況をJavaScriptから知る方法を紹介します。

CSSのサポート状況の確認と処理の振り分け
if('opacity' in style){
  style.opacity = 0.5;
} else/* if('filter' in style) */{
  style.filter = 'alpha(opacity=50)';
}

このように、styleプロパティに対して、対象のスタイルが存在するかどうかをin演算子でチェックします。in演算子はオブジェクトにプロパティが存在するかをチェックすることができる、ECMA-262で定義された演算子です。この方法が最もスマートで間違いがありません。もちろん、要はundefinedかどうかのチェックをしているだけなので、style.opacity !== undefinedや、typeof style.opacity !== 'undefined'のような形でも問題はありません。ただし、ECMA-262 3rdでは undefined が予約語ではない(書き換え可能)ので、前者はundefinedの代わりに void 0(void演算子)を使用したほうが確実です。

なお、このような分岐処理を何度も行うのは非効率なので、次のようにsetOpacity関数を定義しておくというのも良いかもしれません。

CSSのサポート状況の確認と処理の振り分け
var setOpacity = function(elem, value){
  elem.style.opacity = value;
};
if(!('opacity' in document.documentElement.style)){
  setOpacity = function(elem, value){
    elem.style.filter = 'alpha(opacity='+value*100+')';
  };
}
setOpacity(document.body, 0.5);

特に、だんだんと透明にするようなアニメーション処理を行う場合はこういった最適化が効いてきます(ただ、関数呼び出しをしてしまう時点で最速ではありません……⁠⁠。

なお、こういった関数の定義方法はいくつかありますが、第4回 JavaScriptの基礎知識#1のJavaScriptの関数でも説明していますので、未読の方は是非、既読の方はよろしければ復習してみてください。

複数のスタイルを一度に適用する方法

ひとつの要素に対して、一度に複数のスタイルを適用する方法もあります。1つはシンプルにsetAttributeで要素のstyle属性を設定する方法です。

styleプロパティの操作#3
var div = document.getElementById('css-sample2');
div.setAttribute('style','font-size:20px;color:white;background-color:#444;width:100px;padding:10px;');

ただし、この方法はIE 6, IE 7で動作しません。その代わりとして、styleプロパティのcssTextを使う方法があります。

styleプロパティの操作#4
var div = document.getElementById('css-sample2');
div.style.cssText='font-size:20px;color:white;background-color:#444;width:100px;padding:10px;';

cssTextを使う方法はIE 6やSafari 3などを含めてクロスブラウザで動作しますし、速度もsetAttributeと変わらず、このサンプルのように複数のスタイルを変更するケースでは個別に操作するより高速に動作します。

サンプル2

class名によるスタイルの操作

スタイルを操作する方法として要素のクラス名を変更する方法も一般的です。こちらは1つの操作で複数の要素のスタイルに影響を与えることが可能なため、より効率的にスタイルを変更することが可能ですし、逆に非効率にも成り得る方法です。

クラス名の操作
<style>
.css-sample3 div{
  font-size:20px;
  color:white;
  background-color:#444;
  width:100px;
  padding:10px;
  margin:5px;
}
</style>
<div id="css-sample3">
  <div>サンプル1</div>
  <div>サンプル2</div>
  <div>サンプル3</div>
</div>
<script>
(function(){
  var btn  = document.getElementById('css-sample3-btn');
  var div = document.getElementById('css-sample3');
  btn.onclick = function (){
    div.className = 'css-sample3';
  };
})();
</script>
サンプル1
サンプル2
サンプル3

このように、ある要素のclass名を変更することでその要素の子要素のスタイルを一度に変更することが可能です。

この方法は一見効率的に見えますが、クラス名を変更した際の影響範囲に注意が必要です。上記の例ではクラス名を変更した要素の子要素は3つしかありませんが、これがもっとたくさんの要素が含まれていて、しかもそれらのほとんどがスタイルを変更する対象の要素ではなかった場合、その関係の無い要素に対してもクラス名が変更されたことによってスタイルの影響を受けるかどうかの計算が行われます。

さらには、モダンブラウザでは隣接セレクタのなどの多くのセレクタをサポートしているため、その影響範囲は意外なほど大きくなっています。もちろん、モダンブラウザではそれに応じてCSSの処理も高速化しているので、ボトルネックになることはそう多くはありません。ただ、パフォーマンスを改善する際には要素のstyleプロパティを直接操作する方法に変えてみたり、クラス名を変更する要素の兄弟要素・子/子孫要素をなるべく少なくしてみると大きな改善が可能かもしれません。

なお、クラス名ではなくID名を変更することで同様のことが可能ですが、IDはページ内でユニークであるべきもので、それが動的に変更されるというのはおすすめできない状況です。getElementByIdの扱いも難しくなってしまうので、IDは滅多なことでは変更しない方が良いでしょう。

要素のスタイルを取得

続いて、ある要素に適用されている現在のスタイル情報を取得する方法を解説します。まず、Firefox、Chrome、Safari、OperaなどのブラウザではDOM Level 2 Styleで定義されているgetComputedStyleが使用できます。

getComputedStyleの利用
if(!window.getComputedStyle){
  window.getComputedStyle = document.defaultView.getComputedStyle;
}
var style = getComputedStyle(document.body, '');
console.log(style, style.fontSize);

getComputedStyleの第二引数はbefore、 afterなどの擬似要素のスタイルを取得する場合に使用します。それ以外は空文字列を渡します(Firefox以外では省略可能です⁠⁠。

ほとんどのブラウザでgetComputedStyleはwindowオブジェクト(グローバルオブジェクト)のメソッドとして定義されているので、getComputedStyleをいきなり呼び出すことも可能です。ただし、Safari 3ではdocument.defaultViewのみに定義されている(document.defaultViewは実質windowオブジェクトと同じもの)ので、上記のように記述しておくと少し安全です。なお、getComputedStyleで取得できるのは要素のstyleプロパティと同じインターフェースを持ったオブジェクトです。

これに対して、IEは要素自身がcurrentStyleというプロパティを持っており、このcurrentStyleがgetComputedStyleで取得したオブジェクトとほぼ同じものになっています。

currentStyleの利用
var style = document.body.currentStyle;
alert([style, style.fontSize]);

ではこれをクロスブラウザにしてみます。

getComputedStyleのクロスブラウザ定義
if(!window.getComputedStyle){
  if (document.defaultView){
    getComputedStyle = document.defaultView.getComputedStyle;
  } else if(document.documentElement) {
    getComputedStyle = function(element){
      return element.currentStyle;
    };
  }
}
var style = getComputedStyle(document.body, '');
console.log(style, style.fontSize);

このように、getComputedStyleが存在しない場合、代わりにgetComputedStyleを定義するようにしました。ただ、この程度であればわざわざ関数を定義する手間を省いて、ワンライナーで書いてしまってもよいでしょう。

styleの取得イディオム
var style = element.currentStyle || document.defaultView.getComputedStyle(element,'')

なお、getComputedStyleで取得した値は基本的に単位はpxに変換された状態になります。一方、currentStyleは指定されている単位そのままの状態で取得します。どちらがよいかは一長一短ですが、アニメーションさせる場合などではpx単位になっていたほうがやりやすいことが多いようです。

また、このgetComputedStyleは要素のスタイルを強制的に計算させる側面があるため、使いどころによっては大きなボトルネックになりかねません。多くの場合でgetComputedStyleを使わずに済むはずなので、そういった方法がないか検討してみると良いでしょう。

CSSのルールの操作

ここまでに紹介した方法は要素自体を操作してスタイルを変更・取得する方法でした。ここからはCSSのルール自体を操作する方法を解説します。

このルールの操作方法はDOM Level 2 Styleで定義されており、Firefox, Opera, Safari, Chromeなどはこの仕様に沿って実装しています。一方、IE 6~8は独自の仕様で実装しているので処理を分ける必要があります。まずは標準的な方法を見ていきます。

CSSのルールを操作するうえで最初に必要なのはStyleSheetオブジェクトの取得です。ルールの操作はStyleSheetオブジェクトに対して行うので、これがないと始まりません。このStyleSheetオブジェクトは、document.styleSheets から参照できます。このオブジェクトは配列ライクなオブジェクトなので、document.styleSheets[0] で1つ目、document.styleSheets[document.styleSheets.length-1] で最後のStyleSheetオブジェクトを取得できます。

ただし、注意しなければいけないのはこのStyleSheetオブジェクトが外部ドメインのCSSファイルを参照していた場合、ルールに対する操作ができない点です。また、⁠これは事前に回避できる問題ですが)元々ページ内に link[type="text/css"] な要素かstyle要素が存在しない場合、StyleSheetオブジェクトは空の状態です。さらに、どのブラウザも(サードパーティ製などの)ブラウザの機能を拡張する仕組み(以降、拡張機能と呼びます)を持っており、その中にはStyleSheetを追加するものもあります。拡張機能によって追加されたStyleSheetオブジェクトは外部ドメインのStyleSheetと同じくページ側から操作できない可能性があります。

このため、document.styleSheetsからオブジェクトを取得する方法はお勧めできません。その代わりに、StyleSheetオブジェクトを動的に作るとよいでしょう。その方法は簡単で、createElementでstyle要素を作れば、そのsheetプロパティがStyleSheetオブジェクトになっています。

StyleSheetオブジェクトの作成
var style = document.createElement('style');
var head = document.getElementsByTagName('head')[0];
head.appendChild(style);
var sheet = style.sheet;

では、このStyleSheetオブジェクトに対してルールを追加してみます。

ルールの挿入
sheet.insertRule('p{color:red;}', sheet.cssRules.length);

このように、StyleSheetオブジェクトはinsertRuleというメソッドを持っています。このinsertRuleは第一引数にルールを、第二引数にルールを挿入する位置を指定します。第二引数を省略した場合はルールの先頭に追加されてしまいます。セレクタの優先度が同じ場合、後ろにあるルールが適用されるため、最後に挿入されるようにルールの数を取得してinsertRuleに渡しています。

なお、第一引数のルールの部分はセレクタとそのスタイルの組み合わせになっていますが、一度に挿入できるのは1つのスタイル(ルール)のみとなっています。insertRule('p,pre{color:red;}', は可能ですが、insertRule('p{color:red;} pre{color:blue;}', のようにすることはできません。

続いて、IEの独自実装を見ていきます。まず、StyleSheetオブジェクトの作成方法は上記と同様のコードでも動作しますが(ただし、StyleSheetオブジェクトはsheetプロパティではなく、styleSheetプロパティに存在します⁠⁠、IEにはもっと楽な方法が用意されています。

StyleSheetオブジェクトの作成(IE)
var sheet = document.createStyleSheet();

このように、createStyleSheetというまさにStyleSheetオブジェクトを作るためのメソッドが用意されています。こちらは自動的にページに挿入された状態になるので、appendChildなどをする必要がありません。ルールの追加方法もシンプルで、addRuleというメソッドを使用します。

ルールの挿入(IE)
sheet.addRule('p', 'color:red;');

こちらは第一引数でセレクタを、第二引数でスタイルを渡します。第三引数にはinsertRuleと同様に挿入する位置を指定することが可能ですが、省略すれば勝手に最後に追加されるのでほとんどの場合で省略しても問題ありません。

では、クロスブラウザで使えるルール追加関数を定義してみましょう。まず前提として、この関数を使うたびにStyleSheetオブジェクトを作っていたのでは非効率ですから一度だけ作成するようにします(createStyleSheetには31個までしかシートを作成できないという制約もあります⁠⁠。ただし、関数を一度も使わないのにStyleSheetオブジェクトを作成するというのも避けるようにします。つまり、一度目に呼び出された時にStyleSheetオブジェクトを作り、次回からはそのオブジェクトを使いまわすようにします。なお、今回は(非標準ですが)createStyleSheetが使える場合はそちらを使用することにします。

ルール追加関数
function addRule(selector, style){
  var sheet;
  if(!addRule.add){
    if (document.createStyleSheet){
      sheet = document.createStyleSheet();
      addRule.add = function(selector, style){
        return sheet.addRule(selector, style);
      };
    } else {
      var style = document.createElement('style');
      var head = document.getElementsByTagName('head')[0];
      head.appendChild(style);
      sheet = style.sheet;
      addRule.add = function(selector, style){
        return sheet.insertRule(selector+'{'+style+'}', sheet.cssRules.length);
      };
    }
    // 追加したスタイルを無効化する関数
    addRule.disable = function(){
      return sheet.disabled = true;
    };
    addRule.enable = function(){
      return sheet.disabled = false;
    };
  }
  // addRuleが引数ありで呼び出されたかチェック
  if (arguments.length){
    return addRule.add(selector, style);
  } else {
    return addRule;
  }
}

初回呼び出し時にaddRule関数のプロパティとしてadd, disable, enableというメソッドを追加しています。なお、arguments.lengthで引数の数をチェックして、引数なしで呼び出されたときは各メソッドの定義だけをするようにしました。つまり次のように使うことも可能です。

ルール追加方法
addRule().add('p','font-size:120%;');
addRule('p','color:black;');
addRule.add('p','font-weight:bold;');
addRule.disable();// 上記のスタイルを無効化

ちなみに、Opera、Safari、ChromeなどはIEの独自拡張であるaddRuleもサポートしているので、そちらを使用しても問題はありません。

さらにおまけで、DOM Level 2ではcreateCSSStyleSheetというメソッドが定義されています。実際に、Safari, Chromeはdocument.implementation.createCSSStyleSheetというメソッドが定義されています。しかし、このcreateCSSStyleSheetで作ったStyleSheetオブジェクトはページのdocumentに関連付けられておらず、関連付ける方法もないので実用はできません。

CSSの動的な生成

最後に、スタイルシート自体を動的に作る方法を紹介します。やはりIEの場合はcreateStyleSheetで、それ以外はstyle要素を作ります。

クラス名の操作
<div class="css-sample4">
  <div>サンプル1</div>
  <div>サンプル2</div>
  <div>サンプル3</div>
</div>
<script>
(function(){
  var btn  = document.getElementById('css-sample4-btn');
  btn.onclick = function (){
    var css = '.css-sample4 div{'+
    '  font-size:20px;'+
    '  color:white;'+
    '  background-color:#444;'+
    '  width:100px;'+
    '  padding:10px;'+
    '  margin:5px;'+
    '}';
    if(document.createStyleSheet){
      var sheet = document.createStyleSheet();
      sheet.cssText = css;
    } else {
      var style = document.createElement('style');
      style.textContent = css;
      var head = document.getElementsByTagName('head')[0];
      head.appendChild(style);
    }
  };
})();
</script>
サンプル1
サンプル2
サンプル3

上記の通り、CSSをJavaScript内に文字列リテラルで書くのはおすすめできませんし、わざわざJavaScriptからスタイルを作らなければいけないケースというのはそうそうないので、あまり実用的ではないかもしれません。

まとめ

今回はCSSをJavaScriptから操作する方法を紹介しました。DOM Level 2に相当する部分のため、IEとそれ以外で差があるため少々面倒なことになっています。ただ、IE 9ではやはりほかのブラウザと同様の方法で実装できるようになっています。

次回はいよいよXMLHttpRequestやJSONPなどの非同期処理を取り上げる予定です。

おすすめ記事

記事・ニュース一覧