第12章: ドキュメントオブジェクトモデル

第11章では、JavaScriptのオブジェクトがHTMLドキュメントのforminputタグを参照しているのを見ました。このようなオブジェクトは、ドキュメントオブジェクトモデル(DOM)と呼ばれる構造の一部です。ドキュメントのすべてのタグはこのモデルで表現され、検索して操作することができます。

HTMLドキュメントには、階層構造と呼ばれるものがあります。トップの<html>タグを除く各要素(またはタグ)は、別の要素、つまり親に含まれています。この要素は、さらに子要素を含むことができます。これを家系図のようなものとして視覚化することができます。

ドキュメントオブジェクトモデルは、ドキュメントのこのような見方に基づいています。ツリーには2種類の要素が含まれていることに注意してください。青いボックスで示されているノードと、単純なテキストの断片です。テキストの断片は、後で見るように、他の要素とは少し異なる動作をします。たとえば、子を持つことはありません。

画像に示されているドキュメントを含むファイルexample_alchemy.htmlを開き、コンソールをアタッチしてください。

attach(window.open("example_alchemy.html"));

ドキュメントツリーのルートであるhtmlノードのオブジェクトは、documentオブジェクトのdocumentElementプロパティを介してアクセスできます。ほとんどの場合、代わりにドキュメントのbody部分へのアクセスが必要になります。これはdocument.bodyにあります。


これらのノード間のリンクは、ノードオブジェクトのプロパティとして利用できます。すべてのDOMオブジェクトには、それが含まれているオブジェクト(もしあれば)を参照するparentNodeプロパティがあります。これらの親もまた、子を指すリンクを持っていますが、複数の子が存在する可能性があるため、これらはchildNodesと呼ばれる疑似配列に格納されます。

show(document.body);
show(document.body.parentNode);
show(document.body.childNodes.length);

便宜上、ノード内の最初と最後の子を指すfirstChildlastChildというリンクも用意されています。子がない場合はnullになります。

show(document.documentElement.firstChild);
show(document.documentElement.lastChild);

最後に、ノードの「隣」に位置するノード、つまり現在のノードの前または後に来る同じ親の子であるノードを指す、nextSiblingpreviousSiblingというプロパティがあります。ここでも、そのような兄弟がない場合、これらのプロパティの値はnullです。

show(document.body.previousSibling);
show(document.body.nextSibling);

ノードが単純なテキストの断片を表しているのか、実際のHTMLノードを表しているのかを調べるには、nodeTypeプロパティを確認します。これには、通常のノードの場合は1、テキストノードの場合は3の数値が含まれています。実際には、nodeTypeを持つ他の種類のオブジェクト(documentオブジェクトなど。これは9)もありますが、このプロパティの最も一般的な用途は、テキストノードと他のノードを区別することです。

function isTextNode(node) {
  return node.nodeType == 3;
}

show(isTextNode(document.body));
show(isTextNode(document.body.firstChild.firstChild));

通常のノードには、それが表すHTMLタグのタイプを示すnodeNameというプロパティがあります。一方、テキストノードには、テキストコンテンツを含むnodeValueがあります。

show(document.body.firstChild.nodeName);
show(document.body.firstChild.firstChild.nodeValue);

nodeNameは大文字で始まることに注意してください。これは、何かと比較したい場合に考慮する必要があることです。

function isImage(node) {
  return !isTextNode(node) && node.nodeName == "IMG";
}

show(isImage(document.body.lastChild));

例 12.1

DOMノードが与えられたときに、そのノードとその子のHTMLテキストを表す文字列を生成する関数asHTMLを作成します。属性は無視し、ノードは<nodename>として表示します。テキストノードの内容を適切にエスケープするために、第10章escapeHTML関数を利用できます。

ヒント:再帰!

function asHTML(node) {
  if (isTextNode(node))
    return escapeHTML(node.nodeValue);
  else if (node.childNodes.length == 0)
    return "<" + node.nodeName + "/>";
  else
    return "<" + node.nodeName + ">" +
           map(asHTML, node.childNodes).join("") +
           "</" + node.nodeName + ">";
}

print(asHTML(document.body));

実際、ノードにはすでにasHTMLに似たものがあります。それらのinnerHTMLプロパティを使用して、ノード自体のタグを含めずに、ノードの内部のHTMLテキストを取得できます。一部のブラウザは、ノード自体を含むouterHTMLもサポートしていますが、すべてではありません。

print(document.body.innerHTML);

これらのプロパティの一部は変更することもできます。ノードのinnerHTMLまたはテキストノードのnodeValueを設定すると、その内容が変更されます。最初のケースでは、与えられた文字列がHTMLとして解釈され、2番目のケースではプレーンテキストとして解釈されることに注意してください。

document.body.firstChild.firstChild.nodeValue =
  "Chapter 1: The deep significance of the bottle";

または...

document.body.firstChild.innerHTML =
  "Did you know the 'blink' tag yet? <blink>Joy!</blink>";

一連のfirstChildおよびlastChildプロパティを使用してノードにアクセスしてきました。これは機能しますが、冗長で壊れやすいです。ドキュメントの先頭に別のノードを追加すると、document.body.firstChildh1要素を参照しなくなり、それを前提とするコードは間違った動作をします。それに加えて、一部のブラウザでは、タグ間のスペースや改行などのためにテキストノードを追加しますが、他のブラウザでは追加しないため、DOMツリーの正確なレイアウトが異なる場合があります。

この代替案として、アクセスする必要のある要素にid属性を付与する方法があります。例ページでは、画像にID"picture"が設定されており、これを使用して検索できます。

var picture = document.getElementById("picture");
show(picture.src);
picture.src = "img/ostrich.png";

getElementByIdを入力するときは、最後の文字が小文字であることに注意してください。また、何度も入力するときは、手根管症候群に注意してください。document.getElementByIdは非常に一般的な操作にしてはあまりにも長い名前であるため、JavaScriptプログラマーの間では、これを$に積極的に省略するのが慣例となっています。ご存知かもしれませんが、$はJavaScriptでは文字と見なされ、したがって有効な変数名です。

function $(id) {
  return document.getElementById(id);
}
show($("picture"));

DOMノードには、getElementsByTagName(これもまた、素敵な短い名前)というメソッドもあり、タグ名を指定すると、呼び出されたノードに含まれるその型のすべてのノードの配列を返します。

show(document.body.getElementsByTagName("BLINK")[0]);

これらのDOMノードでできることのもう1つは、新しいノードを自分で作成することです。これにより、ドキュメントに自由に要素を追加できるようになり、興味深い効果を生み出すことができます。残念ながら、これを行うためのインターフェースは非常に不格好です。しかし、それはいくつかのヘルパー関数で修正できます。

documentオブジェクトには、createElementメソッドとcreateTextNodeメソッドがあります。最初のメソッドは通常のノードを作成するために使用され、2番目のメソッドは、名前が示すように、テキストノードを作成します。

var secondHeader = document.createElement("H1");
var secondTitle = document.createTextNode("Chapter 2: Deep magic");

次に、タイトル名をh1要素に入れ、次に要素をドキュメントに追加します。これを行う最も簡単な方法は、すべての(テキスト以外の)ノードで呼び出すことができるappendChildメソッドです。

secondHeader.appendChild(secondTitle);
document.body.appendChild(secondHeader);

多くの場合、これらの新しいノードに属性を与えることも必要になります。たとえば、img(画像)タグは、ブラウザに表示する必要がある画像を伝えるsrcプロパティがないとほとんど役に立ちません。ほとんどの属性はDOMノードのプロパティとして直接アクセスできますが、より一般的な方法で属性にアクセスするために使用されるsetAttributeメソッドとgetAttributeメソッドもあります。

var newImage = document.createElement("IMG");
newImage.setAttribute("src", "img/Hiva Oa.png");
document.body.appendChild(newImage);
show(newImage.getAttribute("src"));

しかし、いくつかの単純なノード以上を構築したい場合、document.createElementまたはdocument.createTextNodeへの呼び出しで各ノードを作成し、その属性と子ノードを1つずつ追加するのは非常に面倒になります。幸いなことに、作業のほとんどを代行する関数を作成するのは難しくありません。それを行う前に、注意すべき小さな詳細が1つあります。ほとんどのブラウザでは正常に動作するsetAttributeメソッドは、Internet Explorerでは常に機能するとは限りません。一部のHTML属性の名前はすでにJavaScriptで特別な意味を持っているため、対応するオブジェクトプロパティの名前が調整されています。具体的には、class属性はclassNameになり、forhtmlForになり、checkeddefaultCheckedに名前が変更されます。Internet Explorerでは、setAttributegetAttributeも、元のHTML名ではなく、これらの調整された名前で動作するため、混乱する可能性があります。さらに、この章で後述するclassと並んで、style属性は、そのブラウザではsetAttributeで設定できません。

回避策は次のようになります。

function setNodeAttribute(node, attribute, value) {
  if (attribute == "class")
    node.className = value;
  else if (attribute == "checked")
    node.defaultChecked = value;
  else if (attribute == "for")
    node.htmlFor = value;
  else if (attribute == "style")
    node.style.cssText = value;
  else
    node.setAttribute(attribute, value);
}

Internet Explorerが他のブラウザと異なるすべてのケースで、すべてのケースで機能する何かを行います。詳細について心配する必要はありません。これは、必要ないほうが良いが、非準拠のブラウザが記述を強制する種類の醜いトリックです。これがあれば、DOM要素を構築するための単純な関数を記述することができます。

function dom(name, attributes) {
  var node = document.createElement(name);
  if (attributes) {
    forEachIn(attributes, function(name, value) {
      setNodeAttribute(node, name, value);
    });
  }
  for (var i = 2; i < arguments.length; i++) {
    var child = arguments[i];
    if (typeof child == "string")
      child = document.createTextNode(child);
    node.appendChild(child);
  }
  return node;
}

var newParagraph = 
  dom("P", null, "A paragraph with a ",
      dom("A", {href: "http://en.wikipedia.org/wiki/Alchemy"},
          "link"),
      " inside of it.");
document.body.appendChild(newParagraph);

dom関数はDOMノードを作成します。最初の引数はノードのタグ名を指定し、2番目の引数はノードの属性を含むオブジェクト、または属性が必要ない場合はnullです。その後、任意の数の引数が続き、これらは子ノードとしてノードに追加されます。ここに文字列が現れると、それらは最初にテキストノードに入れられます。


appendChildは、ノードを別のノードに挿入する唯一の方法ではありません。新しいノードを親の最後に表示しない場合は、insertBeforeメソッドを使用して、別のチャイルドノードの前に配置できます。これは、新しいノードを最初の引数として、既存の子を2番目の引数として取ります。

var link = newParagraph.childNodes[1];
newParagraph.insertBefore(dom("STRONG", null, "great "), link);

すでにparentNodeを持っているノードがどこかに配置されると、現在の位置から自動的に削除されます。ノードは、ドキュメント内に複数の場所に存在することはできません。

ノードを別のノードで置き換える必要がある場合は、replaceChildメソッドを使用します。これも、新しいノードを最初の引数として、既存のノードを2番目の引数として取ります。

newParagraph.replaceChild(document.createTextNode("lousy "),
                          newParagraph.childNodes[1]);

そして最後に、子ノードを削除するためのremoveChildがあります。これは、削除するノードのに対して呼び出され、引数として子ノードが渡されることに注意してください。

newParagraph.removeChild(newParagraph.childNodes[1]);

例 12.2

引数として与えられたDOMノードを親ノードから削除する、便利な関数removeElementを書いてください。

function removeElement(node) {
  if (node.parentNode)
    node.parentNode.removeChild(node);
}

removeElement(newParagraph);

新しいノードを作成したり、ノードを移動したりする際には、次のルールに注意する必要があります。ノードは、作成されたドキュメントとは別のドキュメントに挿入することはできません。これは、追加のフレームやウィンドウが開いている場合、一方のドキュメントからドキュメントの一部を取り出して別のドキュメントに移動することはできず、1つのdocumentオブジェクトのメソッドで作成されたノードは、そのドキュメント内にとどまる必要があることを意味します。一部のブラウザ(特にFirefox)ではこの制限が強制されていないため、この制限に違反するプログラムはそれらのブラウザでは正常に動作しますが、他のブラウザでは壊れてしまいます。


このdom関数でできる便利なことの例としては、JavaScriptオブジェクトを受け取り、それらをテーブルにまとめるプログラムがあります。HTMLでは、テーブルはtで始まる一連のタグで作成され、次のようになります。

<table>
  <tbody>
    <tr> <th>Tree </th> <th>Flowers</th> </tr>
    <tr> <td>Apple</td> <td>White  </td> </tr>
    <tr> <td>Coral</td> <td>Red    </td> </tr>
    <tr> <td>Pine </td> <td>None   </td> </tr>
  </tbody>
</table>

tr要素はテーブルの行です。thtd要素はテーブルのセルで、tdは通常のデータセル、thセルは「ヘッダー」セルであり、少し目立つように表示されます。テーブルをHTMLとして記述する場合、tbody(テーブル本体)タグを含める必要はありませんが、DOMノードからテーブルを構築する場合は、tbodyを含める必要があります。これは、Internet Explorerがtbodyなしで作成されたテーブルの表示を拒否するためです。


例 12.3

関数makeTableは、2つの配列を引数として受け取ります。1つ目は、まとめる必要があるJavaScriptオブジェクトが含まれており、2つ目は、テーブルの列名と、これらの列に表示する必要があるオブジェクトのプロパティを名前で示す文字列が含まれています。たとえば、次のようにすると上記のテーブルが生成されます。

makeTable([{Tree: "Apple", Flowers: "White"},
           {Tree: "Coral", Flowers: "Red"},
           {Tree: "Pine",  Flowers: "None"}],
          ["Tree", "Flowers"]);

この関数を作成してください。

function makeTable(data, columns) {
  var headRow = dom("TR");
  forEach(columns, function(name) {
    headRow.appendChild(dom("TH", null, name));
  });

  var body = dom("TBODY", null, headRow);
  forEach(data, function(object) {
    var row = dom("TR");
    forEach(columns, function(name) {
      row.appendChild(dom("TD", null, String(object[name])));
    });
    body.appendChild(row);
  });

  return dom("TABLE", null, body);
}

var table = makeTable(document.body.childNodes,
                      ["nodeType", "tagName"]);
document.body.appendChild(table);

オブジェクトからの値をテーブルに追加する前に、文字列に変換することを忘れないでください。私たちのdom関数は、文字列とDOMノードのみを理解します。


HTMLとドキュメントオブジェクトモデルに密接に関連しているのが、スタイルシートのトピックです。これは大きなトピックであり、すべてを議論するつもりはありませんが、多くの興味深いJavaScriptテクニックにはスタイルシートの理解が必要なので、基本を説明します。

昔ながらのHTMLでは、ドキュメント内の要素の外観を変更する唯一の方法は、要素に追加の属性を与えるか、centerで水平方向に中央揃えにする、またはfontでフォントスタイルや色を変更するなど、追加のタグで要素をラップすることでした。ほとんどの場合、これは、ドキュメント内の段落やテーブルを特定の方法で表示したい場合、それらすべてに大量の属性とタグを追加する必要があったことを意味します。これはすぐにそのようなドキュメントに多くのノイズを追加し、手動で記述したり変更したりするのが非常に面倒になります。

もちろん、人間は発明好きな猿なので、誰かが解決策を思いつきました。スタイルシートは、「このドキュメントでは、すべての段落にComic Sansフォントを使用し、紫にし、すべてのテーブルには太い緑色の境界線がある」というようなステートメントを作成する方法です。ドキュメントの先頭または別のファイルで一度指定すると、ドキュメント全体に影響します。たとえば、ヘッダーを22ポイントの大きさにし、中央揃えにし、段落を以前に述べたフォントと色にし、それが「醜い」クラスの場合のスタイルシートは次のようになります。

<style type="text/css">
  h1 {
    font-size: 22pt;
    text-align: center;
  }

  p.ugly {
    font-family: Comic Sans MS;
    color: purple;
  }
</style>

クラスは、スタイルに関連する概念です。醜い段落や素晴らしい段落など、異なる種類の段落がある場合、すべてのp要素のスタイルを設定することは望ましくないため、クラスを使用してそれらを区別できます。上記のスタイルは、次のような段落にのみ適用されます。

<p class="ugly">Mirror, mirror...</p>

そして、これは、setNodeAttribute関数で簡単に言及したclassNameプロパティの意味でもあります。style属性は、要素に直接スタイルを追加するために使用できます。たとえば、これにより、画像に幅4ピクセル('px')のソリッドボーダーが与えられます。

setNodeAttribute($("picture"), "style",
                 "border-width: 4px; border-style: solid;");

スタイルにはさらに多くのことがあります。一部のスタイルは親ノードから子ノードに継承され、複雑で興味深い方法で相互に干渉しますが、DOMプログラミングの目的のために、最も重要なことは、各DOMノードにstyleプロパティがあり、そのノードのスタイルを操作するために使用できること、およびノードに特別なことをさせるために使用できるいくつかの種類のスタイルがあることを知ることです。

このstyleプロパティは、スタイルの可能なすべての要素のプロパティを持つオブジェクトを参照します。たとえば、画像の境界線を緑色にすることができます。

$("picture").style.borderColor = "green";
show($("picture").style.borderColor);

スタイルシートでは、単語はborder-colorのようにハイフンで区切られていますが、JavaScriptでは、borderColorのように異なる単語をマークするために大文字が使用されていることに注意してください。

非常に実用的なスタイルの1つに、display: noneがあります。これは、ノードを一時的に非表示にするために使用できます。style.display"none"の場合、要素はドキュメントの閲覧者にはまったく表示されませんが、実際には存在します。後で、displayを空の文字列に設定すると、要素が再表示されます。

$("picture").style.display = "none";

そして、画像を元に戻します。

$("picture").style.display = "";

興味深い方法で悪用できるもう1つのスタイルの種類は、位置に関連するものです。単純なHTMLドキュメントでは、ブラウザがすべての要素の画面位置の決定を担当します。各要素は、前にある要素の隣または下に配置され、ノードは(一般的に)重複しません。

positionスタイルが"absolute"に設定されている場合、ノードは通常のドキュメント「フロー」から取り出されます。ドキュメント内のスペースを占有しなくなり、その上に浮かぶようになります。その後、leftおよびtopスタイルを使用して、その位置に影響を与えることができます。これは、ノードをマウスカーソルを追跡させたり、ドキュメントの残りの部分の上に「ウィンドウ」を開いたりするなど、さまざまな目的に使用できます。

$("picture").style.position = "absolute";
var angle = 0;
var spin = setInterval(function() {
  angle += 0.1;
  $("picture").style.left = (100 + 100 * Math.cos(angle)) + "px";
  $("picture").style.top = (100 + 100 * Math.sin(angle)) + "px";
}, 100);

三角法に精通していない場合は、コサインとサインのものが円の輪郭上にある座標を構築するために使用されることを信じてください。1秒間に10回、画像の配置角度が変更され、新しい座標が計算されます。このようなスタイルを設定する際に、値に"px"を追加するのを忘れるのが一般的なエラーです。ほとんどの場合、単位なしでスタイルを数値に設定しても機能しないため、ピクセルには"px"、パーセントには"%"、 'ems'(M文字の幅)には"em"、ポイントには"pt"を追加する必要があります。

(これで画像を再び休ませます…)

clearInterval(spin);

これらの位置の目的で0,0として扱われる場所は、ドキュメント内のノードの場所によって異なります。position: absoluteまたはposition: relativeを持つ別のノード内に配置されている場合、このノードの左上が使用されます。それ以外の場合は、ドキュメントの左上隅が表示されます。


操作するのが楽しいDOMノードの最後の側面は、そのサイズです。widthheightと呼ばれるスタイルタイプがあり、これらを使用して要素の絶対サイズを設定できます。

$("picture").style.width = "400px";
$("picture").style.height = "200px";

ただし、要素のサイズを正確に設定する必要がある場合は、考慮すべき厄介な問題があります。一部のブラウザでは、状況によっては、これらのサイズは、境界線や内部パディングを含むオブジェクトの外側のサイズを意味すると解釈されます。別のブラウザでは、状況によっては、オブジェクトの内部のスペースのサイズを使用し、境界線やパディングの幅をカウントしません。したがって、境界線やパディングを持つオブジェクトのサイズを設定すると、常に同じサイズで表示されるとは限りません。

幸いなことに、ノードの内側と外側のサイズを調べることができます。これは、何かを正確にサイズ設定する必要がある場合に、ブラウザの動作を補正するために使用できます。offsetWidthプロパティとoffsetHeightプロパティは、要素の外側のサイズ(ドキュメントで占有するスペース)を与え、clientWidthプロパティとclientHeightプロパティは、内部のスペース(もしあれば)を与えます。

print("Outer size: ", $("picture").offsetWidth,
      " by ", $("picture").offsetHeight, " pixels.");
print("Inner size: ", $("picture").clientWidth,
      " by ", $("picture").clientHeight, " pixels.");

この章のすべての例を試し、おそらく自分でいくつかの追加のことを行った場合、最初に始めた貧弱な小さなドキュメントを完全に傷つけていることでしょう。ここで少し説教して、実際のページでこれを行わないでください。あらゆる種類の動くキラキラを追加したいという誘惑は、時々強くなるでしょう。それに抵抗してください。そうしないと、ページが読みにくくなったり、行き過ぎると、時々発作を引き起こすことさえあります。