japan.internet.com
japan.internet.com メンバーID
Twitter
Facebook
RSS
ピックアップ
2006年2月14日 10:10

XMLとXSLを使った高度なUIデザイン:パート2

著者Joe Slovinskiオリジナル版を読む海外海外発

はじめに

 この連載では、XMLとXSLを使った高度なUIデザインに挑戦します。1回に1つずつ、XMLとXSL(XML用のスタイルシート言語)を使用して高度なユーザーインタフェース(UI)コンポーネントを作成していきます。

 今回はXMLとXSLTを使い、個々のオブジェクト用に深さ無制限のカスタムコンテキストメニューを作成してみましょう。以下で説明するコンテキストメニューは、XMLフィードとして与えられるフォーマットにXSLTスタイルシートを適用し、その適用結果をクライアントに書き出すことで機能します。メニュー内の移動要求は、すべてクライアント側で処理されます。本稿で使用するクライアントはInternet Explorer 5.5以降です。

 このコンテキストメニューは、Webページ中のどのオブジェクトにでも適用できます。本稿では1つの例として、フォルダツリー内のオブジェクトにコンテキストメニューを適用してみます。

 今回使用するのは、この連載のパート1で作成したフォルダツリーです。パート1でも述べたとおり、このツリーのXMLに含まれる各エンティティには、「onContextMenu」と呼ばれる特殊な要素が追加されています。onContextMenu要素は、要求されたコンテキストメニューの構成を定義しているXMLファイルを参照します。

 実際に動作するコンテキストメニューのデモを見たい方は、ダウンロードサンプルをご利用ください。このサンプルには、コンテキストメニューを構成する一連のファイル(XML、XSL、JS、CSS、HTMLの各ファイルと、画像ファイル)が含まれています。

コンテキストメニューの概要

 Windowsアプリケーションでは、普通、オブジェクトを右クリックすると、そのオブジェクトに固有のコンテキストメニューが表示されます。たとえば、SQL Serverテーブルの行やExcelスプレッドシートの行はもちろん、単にコンピュータのデスクトップを右クリックしても、それぞれに固有のカスタムコンテキストメニューが現れます。

 コンテキストメニューには、使用中のアプリケーションと、そのアプリケーション中でクリックされたオブジェクトに応じたオプションが並びます。初期のWebブラウザでは、開発者がオブジェクトごとにコンテキストメニューを作成することなどできませんでしたが、DOM(ドキュメントオブジェクトモデル)とWebブラウザの進歩により、それがWebアプリケーションで可能となりました。

図1 MS Internet Explorer内に作成されたオブジェクト固有コンテキストメニューの例
図1 MS Internet Explorer内に作成されたオブジェクト固有コンテキストメニューの例

 この例では、番号体系によってメニューのレベルを表しています。

XML構造

 私が選んだXMLフォーマットは、再帰的XSLTスタイルシートに適したフォーマットです。本稿で取り上げる深さ無制限のコンテキストメニューの諸条件をよく満たしています。

 私が作成したXMLドキュメントには、menuという名前のルート要素が含まれています。これはentity要素だけを含むことができます。オプション要素をこれらのentity要素内のcontents要素の入れ子とすることで、コンテキストメニューの構造が暗黙裏に定義されます。次に示すのは、オプション要素に属するすべての要素と属性のリストです。

名前種類内容
id属性個々のオプションを指し示すのに用いられる一意の文字列または整数です。
description要素個々のオプションの記述。このテキストがユーザーに表示されます。
onClick要素onClickイベントで実行されるクライアント側機能の名前です。
image要素オプションが閉じたとき、または選択されなかったときに表示される画像です。
imageOpen要素オプションが開いたときに表示される画像です。
contents要素entity要素を含みます。オプションにサブメニューがあるかどうかを調べるのに使われます。

 次のXMLで記述されているのは、ユーザーが「Customer」を右クリックしたときに表示されるコンテキストメニューです。わかりやすいように静的XMLドキュメントとしていますが、XMLデータベースクエリによって取り出す形にもできます。

<?xml version="1.0"?>
<menu>
  <entity id="c1">
    <description>Add Customer</description>
    <image>images/add_small.gif</image>
    <imageOpen>images/add_small.gif</imageOpen>
    <contents>
      <entity id="c2">
        <description>Business</description>
        <image>images/spacer.gif</image>
        <imageOpen>images/spacer.gif</imageOpen>
        <contents>
        </contents>
      </entity>
      <entity id="c3">
        <description>Individual</description>
        <image>images/spacer.gif</image>
        <imageOpen>images/spacer.gif</imageOpen>
        <contents>
        </contents>
      </entity>
    </contents>
  </entity>
  <entity id="c4">
    <description>Modify Customer</description>
    <image>images/modify_small.gif</image>
    <imageOpen>images/modify_small.gif</imageOpen>
    <contents>
    </contents>
  </entity>
  <entity id="c5">
    <description>Remove Customer</description>
    <image>images/x_small.gif</image>
    <imageOpen>images/x_small.gif</imageOpen>
    <contents>
    </contents>
  </entity>
</menu>

 このXMLファイルの名前は「contextCustomer.xml」です。このファイルはダウンロードサンプルに含まれています。

XSLスタイルシート

 このXMLドキュメントに適用される標準のXSLTスタイルシートは次のとおりです。

<xsl:stylesheet version="1.1"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
xmlns:dt="urn:schemas-microsoft-com:datatypes">
<xsl:template match="menu">
<div style="position: absolute;">
  <div onselectstart="return false" ondragstart="return false">
  <xsl:attribute name="STYLE">
    position: absolute;
    background-color: #6699cc;
    border:1px solid #99ccff;
  </xsl:attribute>
  <table border="0" cellspacing="0" cellpadding="1">
    <tr>
      <td>
        <table border="0" cellspacing="0" cellpadding="0">
          <xsl:apply-templates select="entity"/>
        </table>
      </td>
    </tr>
  </table>
  </div>
  <xsl:apply-templates select="entity/contents"/>
</div>
</xsl:template>

<xsl:template match="entity">
<TR>
<xsl:attribute name="selected">false</xsl:attribute>
<xsl:attribute name="background">#6699cc</xsl:attribute>
<xsl:attribute name="light">#99ccff</xsl:attribute>
<xsl:attribute name="titlebar">#5389bc</xsl:attribute>
<xsl:attribute name="image">images/<xsl:value-of select="image"/>
</xsl:attribute>
<xsl:attribute name="imageOpen">images/
<xsl:value-of select="imageOpen"/></xsl:attribute>
<xsl:attribute name="id"><xsl:value-of select="@id"/></xsl:attribute>
<xsl:attribute name="ONCLICK">
<xsl:value-of select="onClick"/>;clean()</xsl:attribute>
<xsl:attribute name="ONMOUSEOVER">
  contextHighlightRow(this);
  <xsl:if test="contents/node()[count(child::*)>0]">
    loadContextMenuSub(this)
  </xsl:if>
</xsl:attribute>
<xsl:attribute name="ONMOUSEOUT">contextHighlightRow(this)
</xsl:attribute>
  <TD VALIGN="MIDDLE" ALIGN="CENTER" NOWRAP="true">
  <xsl:attribute name="ONCLICK">
<xsl:value-of select="@onmousedown"/></xsl:attribute>
  <xsl:attribute name="STYLE">
    background-color: #5389bc;
    border-top:1px solid #5389bc;
    border-bottom:1px solid #5389bc;
    border-left:1px solid #5389bc;
    padding-left: 4px;
    padding-right: 4px;
    padding-top: 4px;
    padding-bottom: 3px;
    cursor: default;
  </xsl:attribute>
  <IMG BORDER="0" HEIGHT="15" WIDTH="15">
    <xsl:attribute name="SRC"><xsl:value-of select="image"/>
    </xsl:attribute>
  </IMG></TD>
  <TD NOWRAP="true">
  <xsl:attribute name="ONCLICK"><xsl:value-of select="@onmousedown"/>
  </xsl:attribute>
  <xsl:attribute name="STYLE">
    font-family: Arial;
    font-size: 11px;
    font-weight: normal;
    color: white;
    background-color: #6699cc;
    border-top: 1px solid #6699cc;
    border-bottom: 1px solid #6699cc;
    padding-top: 2px;
    padding-bottom:2px;
    padding-left: 6px;
    padding-right: 8px;
    cursor: default;
  </xsl:attribute>
  <xsl:value-of select="description"/></TD>
  <TD VALIGN="middle" ALIGN="right" 
      STYLE="padding-right: 6px;" NOWRAP="true">
  <xsl:attribute name="ONCLICK">
  <xsl:value-of select="@onmousedown"/></xsl:attribute>
  <xsl:attribute name="STYLE">
    background-color: #6699cc;
    border-top: 1px solid #6699cc;
    border-bottom: 1px solid #6699cc;
    border-right: 1px solid #6699cc;
    padding-right: 5px;
  </xsl:attribute>
  <IMG BORDER="0" WIDTH="4">
  <xsl:attribute name="SRC">
    <xsl:choose>
      <xsl:when test="contents/node()[count(child::*)>0]">
        images/opensub.gif
      </xsl:when>
      <xsl:otherwise>
        images/spacer.gif
      </xsl:otherwise>
    </xsl:choose>
  </xsl:attribute>
  </IMG></TD>
</TR>
</xsl:template>

<xsl:template match="contents">
  <xsl:if test="count(child::*)>0">
  <div onselectstart="return false" ondragstart="return false">
  <xsl:attribute name="STYLE">
    position: absolute;
    background-color: #6699cc;
    border:1px solid #99ccff; 
    display: none;
  </xsl:attribute>
  <xsl:attribute name="ID"><xsl:value-of select="../@id"/>Sub
  </xsl:attribute>
  <table border="0" cellspacing="0" cellpadding="1">
    <tr>
      <td>
        <table border="0" cellspacing="0" cellpadding="0">
          <xsl:apply-templates select="entity"/>
        </table>
      </td>
    </tr>
  </table>
  </div>
  <xsl:apply-templates select="entity/contents"/>
  </xsl:if>
</xsl:template>

</xsl:stylesheet>

 図2に、本稿のファイルを使って作成したコンテキストメニューを示しておきます。

図2 XSL変換
図2 XSL変換

クライアント側操作

 このコンテキストメニューをそれらしく動作させるには、クライアント側で次の操作が必要です。

  1. コンテキストメニューのロード
  2. コンテキストサブメニューのロード
  3. 行のハイライト
  4. 消去(開いているコンテキストメニューの除去)

 この4つの操作を実行するコードを次に示しておきます。このコードは、ダウンロードサンプルの「context.js」ファイルに含まれています。

var appState = new applicationState()

function applicationState() {
  this.contextMenu = null
}

function loadContextMenu(path) {
  var xmlDoc
  var xslDoc
  var contextMenu

  if(path != "") {
    xmlDoc = new ActiveXObject(’Microsoft.XMLDOM’)
    xmlDoc.async = false;

    xslDoc = new ActiveXObject(’Microsoft.XMLDOM’)
    xslDoc.async = false;

    xmlDoc.load(path)
    xslDoc.load("context/context.xsl")

    if(appState.contextMenu != null) 
appState.contextMenu.removeNode(true)
  
    document.body.insertAdjacentHTML("beforeEnd", 
xmlDoc.documentElement.transformNode(xslDoc))
    contextMenu = document.body.childNodes(
document.body.childNodes.length-1)

    contextMenu.style.left = window.event.x
    contextMenu.style.top = window.event.y

    appState.contextMenu = contextMenu
    window.event.cancelBubble = true
  }
}

function loadContextMenuSub(obj) {
  var contextMenu
  var parentMenu

  parentMenu = returnContainer(obj)
  contextMenu = document.all[obj.id + "Sub"]
  contextMenu.style.display = "block"
  contextMenu.style.top = obj.offsetTop + parentMenu.style.pixelTop
  contextMenu.style.left = obj.offsetWidth + parentMenu.style.pixelLeft
  parentMenu.subMenu = contextMenu
}

function contextHighlightRow(obj) {
  var parentMenu
  var subMenu
  var i

  parentMenu = returnContainer(obj)

  if(obj.selected == "false") {
    for(i=0; i < obj.childNodes.length; i++) {
      obj.childNodes(i).style.borderTop = "1px solid white"
      obj.childNodes(i).style.borderBottom = "1px solid white"

      if(obj.childNodes(i).cellIndex == 0) {
        obj.childNodes(i).style.borderLeft = "1px solid white"
      }
      else if (obj.childNodes(i).cellIndex == obj.cells.length-1) {
        obj.childNodes(i).style.borderRight = "1px solid white"
      }
    }

    if(parentMenu.subMenu != null && parentMenu != parentMenu.subMenu) {
      subMenu = parentMenu.subMenu

      while(subMenu != null) {
        subMenu.style.display = "none"
        subMenu = subMenu.subMenu
      }
    }
    obj.selected = "true"
  }
  else {
    for(i=0; i < obj.childNodes.length; i++) {
      if(i == 0) {
        obj.childNodes(i).style.borderTop = 
"1px solid " + obj.titlebar
        obj.childNodes(i).style.borderBottom = 
"1px solid " + obj.titlebar
      }
      else {
        obj.childNodes(i).style.borderTop = 
"1px solid " + obj.background
        obj.childNodes(i).style.borderBottom = 
"1px solid " + obj.background
      }
      
      if(obj.childNodes(i).cellIndex == 0) {
        obj.childNodes(i).style.borderLeft = 
"1px solid " + obj.titlebar
      }
      else if (obj.childNodes(i).cellIndex == obj.cells.length-1) {
        obj.childNodes(i).style.borderRight = 
"1px solid " + obj.background
      }
    }
    obj.selected = "false"
  }
}

function clean() {
  var contextMenu
  
  // remove and destroy context menu
  if(appState.contextMenu != null) {
    contextMenu = appState.contextMenu.removeNode(true)
    contextMenu = null
  }
}

function returnContainer(container) {
  while(container.tagName != "DIV") {
    container = container.parentNode  
  }
  return container
}

終わりに

 本稿の内容が、Webアプリケーションインタフェースの質的向上に役立てば幸いです。質問・コメント・提案があれば、筆者紹介にあるアドレスまで遠慮なくメールをお送りください。

 この原稿に目を通し、表現と内容の両方をチェックしてくれたLee McGrawに感謝します。

 次回パート3では、パート1の記事を発展させて、フォルダツリー内の項目を挿入・変更・削除・改名する方法を考えます。

本連載のその他の記事

著者紹介

Joe Slovinski(Joe Slovinski)
1993年以来、Webアプリケーションの開発に継続的に携わる。本稿で紹介したコードについてのお問い合わせはJoe Slovinskiまで。

プリンター用
記事を転送
この記事をクリップ!
厳選した九州のお野菜とお米をお届け
厳選した九州のお野菜とお米をお届け 野菜の木では、老舗料亭 沙羅の木が厳選した九州のお野菜とお米をお届けします。 毎週、隔週での定期のご購入も可能です。 入会費、年会費、送料、荷造手数料は無料です。
注目のトピックス
Copyright 2012 internet.com K.K. (Japan) All Rights Reserved.