はじめに
この連載では、XMLとXSLを使った高度なUIデザインに挑戦します。1回に1つずつ、XMLとXSL(XML用のスタイルシート言語)を使用して高度なユーザーインタフェース(UI)コンポーネントを作成していきます。
今回は第1回として、XMLとXSLで深さ無制限のフォルダツリーを作成してみましょう。以下で説明するフォルダツリーは、XMLフィードとして与えられるフォーマットにXSLスタイルシートを適用し、その適用結果をクライアントに書き出すことで作成されます。ツリー自体とそこに含まれる諸エンティティの展開と折り畳み・最大化と最小化の要求は、すべてクライアント側で処理されます。本稿で使用するクライアントはInternet Explorer 5.5以降です。
作成したフォルダツリーのデモをサンプルとして用意しています。ダウンロードして、実行してみてください。このダウンロードサンプルには、フォルダツリーを構成する諸ファイル(「tree.xsl」「tree.xml」「tree.js」「tree.css」「common.js」および画像ファイル)が含まれています。
フォルダツリーに固有のアーキテクチャ
Webページオブジェクトには、いく通りもの作成方法があるのが普通で、フォルダツリーも同様です。フォルダツリーの開発では、フォルダツリー独特のアーキテクチャ条件に留意してください。たとえば、次のような条件です。
- ネスティング(入れ子)
- 関係線
ネスティング
図1と図2では、DOM(ドキュメントオブジェクトモデル)内にある各オブジェクトの相互関係とネスティング状態をわかりやすくするために、フォルダツリーに一時的に境界線を描いています。
関係線
ツリー内部に関係線を引けば、見映えがよくなるだけでなく、大きなツリーの構造が把握しやすくなります。反面、パフォーマンスが低下するというマイナスもあります。100個の項目を含むツリーでは、エンティティ間の関係を表示するだけで約300個の画像が必要になります。画像のキャッシュ保存やプリロードを行うことで多少はパフォーマンス低下を防ぐことができますが、それでも、画像がDOM内に独立のオブジェクトとして存在することは変わりません。関係線なしのインデントだけでも親子関係を示すには十分ですから、本稿では、関係線のないツリーでいくことにします。
XML構造
ツリーに何を求めるかは、それこそ千差万別です。私がツリーに望むこととあなたが望むことの間には、きっと大きな違いがあるでしょう。ツリーエンティティは、ツリーごとに特異です。そうした特異なツリーエンティティを記述するのに、XMLは最適なインタフェースを備えています。どのフォーマットを選ぶかで、XSLスタイルシートとクライアント動作が大きく変わります。私が選んだのは、再帰的XSLスタイルシートに適したフォーマットです。これは、本稿で取り上げている深さ無限のフォルダツリーという条件をよく満たしています。
私が作成したXMLドキュメントには、treeという名前のルート要素が含まれています。これはentity要素だけを含むことができます。これらのentity要素を、他のentity要素内のcontents要素の入れ子とすることで、ツリーの構造が暗黙裏に定義されます。次に示すのは、entity要素に属するすべての要素と属性のリストです。
| 名前 | 種類 | 内容 |
id | 属性 | 個々のエンティティを指し示すのに用いられる一意の文字列または整数です。 |
description | 要素 | 個々のエンティティの記述。ユーザーに表示されるテキストです。 |
onClick | 要素 | onClickイベントで実行されるクライアント側関数の名前です。 |
image | 要素 | エンティティが閉じたとき、または選択されなかったときに表示される画像です。 |
imageOpen | 要素 | エンティティが開いたときに表示される画像です。 |
contents | 要素 | エンティティ要素を含みます。あるエンティティに子があるかどうかを調べるのに使われます。 |
実際のツリーでは、ツリー内部の各オブジェクトに関する具体的データを、エンティティ要素によって付加していきます。具体的データとは、たとえば、そのエンティティによって表されるデータベースレコードのIDかもしれません。次回以降では、各エンティティ固有のコンテキストを作成し、それをoncontextmenu要素として実装する(エンティティ要素に追加していく)方法を紹介します。
本稿で使用するXMLは次のとおりです。ここでは、わかりやすいように静的XMLドキュメントとして作成していますが、動的XMLデータベースクエリとして実装することもできます。
<?xml version="1.0"?>
<tree>
<entity id="e1">
<description>Customers</description>
<oncontextmenu></oncontextmenu>
<image>images/book.gif</image>
<imageOpen>images/bookOpen.gif</imageOpen>
<contents>
<entity id="e2">
<description>Microsoft</description>
<image>images/book.gif</image>
<imageOpen>images/bookOpen.gif</imageOpen>
<onClick>displayCustomer(12345)</onClick>
<contents>
<entity id="e3">
<description>Orders</description>
<image>images/book.gif</image>
<imageOpen>images/bookOpen.gif</imageOpen>
<onClick></onClick>
<contents/>
</entity>
</contents>
</entity>
<entity id="e4">
<description>IBM</description>
<image>images/book.gif</image>
<imageOpen>images/bookOpen.gif</imageOpen>
<onClick>displayCustomer(12346)</onClick>
<contents>
<entity id="e5">
<description>Orders</description>
<image>images/book.gif</image>
<imageOpen>images/bookOpen.gif</imageOpen>
<onClick></onClick>
<contents/>
</entity>
</contents>
</entity>
<entity id="e6">
<description>Sun Microsystems</description>
<image>images/book.gif</image>
<imageOpen>images/bookOpen.gif</imageOpen>
<onClick>displayCustomer(12347)</onClick>
<contents>
<entity id="e7">
<description>Orders</description>
<image>images/book.gif</image>
<imageOpen>images/bookOpen.gif</imageOpen>
<onClick></onClick>
<contents>
<entity id="e8">
<description>#12345</description>
<image>images/paper.gif</image>
<imageOpen>images/paper.gif</imageOpen>
<onClick></onClick>
<contents/>
</entity>
<entity id="e9">
<description>#12346</description>
<image>images/paper.gif</image>
<imageOpen>images/paper.gif</imageOpen>
<onClick></onClick>
<contents/>
</entity>
</contents>
</entity>
</contents>
</entity>
<entity id="e10">
<description>Oracle</description>
<image>images/book.gif</image>
<imageOpen>images/bookOpen.gif</imageOpen>
<onClick>displayCustomer(12348)</onClick>
<contents>
<entity id="e11">
<description>Orders</description>
<image>images/book.gif</image>
<imageOpen>images/bookOpen.gif</imageOpen>
<onClick></onClick>
<contents/>
</entity>
</contents>
</entity>
</contents>
</entity>
<entity id="e12">
<description>Reports</description>
<oncontextmenu></oncontextmenu>
<image>images/book.gif</image>
<imageOpen>images/bookOpen.gif</imageOpen>
<contents>
<entity id="e13">
<description>Income</description>
<oncontextmenu></oncontextmenu>
<image>images/paper.gif</image>
<imageOpen>images/paper.gif</imageOpen>
<contents>
</contents>
</entity>
<entity id="e14">
<description>Expenses</description>
<oncontextmenu></oncontextmenu>
<image>images/paper.gif</image>
<imageOpen>images/paper.gif</imageOpen>
<contents>
</contents>
</entity>
</contents>
</entity>
</tree>
このXMLファイルの名前は「tree.xml」です。このファイルはダウンロードサンプルに含まれています。
XSLスタイルシート
図1と図2は、どちらも、XMLドキュメントにXSLスタイルシートを適用して得られた結果でした。XMLドキュメントに適用される標準のXSLスタイルシートは次のとおりです。
<xsl:stylesheet xmlns:xsl="http://www.w3.org/TR/WD-xsl"
language="JavaScript">
<xsl:template match="tree">
<xsl:apply-templates select="entity"/>
</xsl:template>
<xsl:template match="entity">
<div onclick="window.event.cancelBubble = true;clickOnEntity(this);"
onselectstart="return false" ondragstart="return false">
<xsl:attribute name="image"><xsl:value-of select="image"/>
</xsl:attribute>
<xsl:attribute name="imageOpen"><xsl:value-of select="imageOpen"/>
</xsl:attribute>
<xsl:attribute name="open">false</xsl:attribute>
<xsl:attribute name="id">f<xsl:value-of select="@id"/>
</xsl:attribute>
<xsl:attribute name="open">false</xsl:attribute>
<xsl:attribute name="STYLE">
padding-left: 20px;
cursor: hand;
<xsl:if expr="depth(this) > 2">
display: none;
</xsl:if>
</xsl:attribute>
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td valign="middle">
<img border="0" id="image">
<xsl:attribute name="SRC">
<xsl:value-of select="image"/>
</xsl:attribute>
</img>
</td>
<td valign="middle" nowrap="true">
<xsl:attribute name="STYLE">
padding-left: 7px;
font-family: Verdana;
font-size: 11px;
font-color: black;
</xsl:attribute>
<xsl:value-of select="description"/></td>
</tr>
</table>
<xsl:apply-templates select="contents/entity"/>
</div>
</xsl:template>
</xsl:stylesheet>
このXSLスタイルシートを使って作成したフォルダツリーを図3に示します。
クライアント側操作
このフォルダツリーをそれらしく動作させるには、クライアント側で次の操作が必要です。
- 初期化
- 展開
- 折り畳み
- 完全展開(最大化)
- 完全折り畳み(最小化)
この5つの操作を実行するコードを次に示しておきます。このコードは、ダウンロードサンプルの「tree.js」ファイルに含まれています。
function initialize() {
var xmlDoc
var xslDoc
xmlDoc = new ActiveXObject(’Microsoft.XMLDOM’)
xmlDoc.async = false;
xslDoc = new ActiveXObject(’Microsoft.XMLDOM’)
xslDoc.async = false;
xmlDoc.load("tree/tree.xml")
xslDoc.load("tree/tree.xsl")
folderTree.innerHTML = xmlDoc.documentElement.transformNode(xslDoc)
}
function clickOnEntity(entity) {
if(entity.open == "false") {
expand(entity, true)
}
else {
collapse(entity)
}
window.event.cancelBubble = true
}
function expand(entity) {
var oImage
oImage = entity.childNodes(0).all["image"]
oImage.src = entity.imageOpen
for(i=0; i < entity.childNodes.length; i++) {
if(entity.childNodes(i).tagName == "DIV") {
entity.childNodes(i).style.display = "block"
}
}
entity.open = "true"
}
function collapse(entity) {
var oImage
var i
oImage = entity.childNodes(0).all["image"]
oImage.src = entity.image
// collapse and hide children
for(i=0; i < entity.childNodes.length; i++) {
if(entity.childNodes(i).tagName == "DIV") {
if(entity.id != "folderTree")
entity.childNodes(i).style.display = "none"
collapse(entity.childNodes(i))
}
}
entity.open = "false"
}
function expandAll(entity) {
var oImage
var i
expand(entity, false)
// expand children
for(i=0; i < entity.childNodes.length; i++) {
if(entity.childNodes(i).tagName == "DIV") {
expandAll(entity.childNodes(i))
}
}
}
Webベースのフォルダツリーの今後の展開
Web技術の進歩にともない、Webアプリケーション内部にステート情報を維持できるようになって、しだいに多くの機能がサーバ側からクライアント側に移され、インタフェースは複雑になる一方です。しかし、その結果、サーバの負担が軽くなり、インタフェースの使い勝手とアプリケーションのパフォーマンスが向上するわけですから、喜ぶべきことでもあります。
いずれ、本稿で作成したツリーをさらに発展させ、ツリー内部にあるオブジェクトの挿入・削除・改名・再表示を動的に行うAPIを作成して、紹介したいと考えています。
終わりに
本稿の内容が、Webアプリケーションインタフェースの質的向上に役立てば幸いです。質問・コメント・提案があれば、筆者紹介にあるアドレスまで遠慮なくメールをお送りください。
この原稿に目を通し、表現と内容の両方をチェックしてくれたLee McGrawに感謝します。
次回は、カスタムコンテキストメニューの作成を取り上げます。
本連載のその他の記事