コンテキストメニュー
コンテキストメニューとは、メニュー上のアイテムが、メニューが開かれたコンテキスト (状況、文脈) に特有のものであるようなメニューの事です。ユーザがある要素を右クリックした時に、その要素に関係のあるコマンドのメニューを表示させるためによく使われます。
コンテキストメニューイベント
コンテキストメニューを開く方法は色々あります。最も一般的なのは要素を右マウスボタンでクリックする方法です。マウスボタンが 1 つしかない Macintosh システムでは、マウスボタンを押し続けるか、Control キーを押しながらマウスボタンをクリックするとコンテキストメニューが開かれます。Windows では、キーボードのメニューキー (多くのキーボードで Control キーの隣にある、メニューの絵が描かれたキー) を押すか、Shift + F10 キーを押してコンテキストメニューを開く事も出来ます。ユーザがコンテキストメニューを開くのにマウスを使うものだと決めてかかってはいけません。
マウスを使ってコンテキストメニューを開いた場合、コンテキストはクリックされた要素になります。キーボードを使った場合には、コンテキストはウィンドウの中で現在フォーカスされている要素になります。
コンテキストメニューが開かれるにはいくつか異なる方法があるので、それら全ての状況を捉えるのに単一のイベントが使われます。どの場合でも、「contextmenu」イベントが発生します。
<hbox id="container" align="center" oncontextmenu="..."> <label value="Name:"/> <textbox id="name"/> </hbox>
この例では、hbox のどの部分でコンテキストメニューを開こうとしても、oncontextmenu
属性を使って取り付けられたイベントリスナが呼び出されます。textbox には組み込みのコンテキストメニューがあるので、そのコンテキストメニューを開こうとする前にイベントが発生します。しかし、イベントは例えば label が右クリックされた時にも発生するので、イベントハンドラは contextmenu イベントのターゲットが目的のものであるかを確かめる必要があります。
イベントオブジェクトの preventDefault メソッドを使うと、contextmenu イベントをキャンセルする事ができます。
function checkContextMenu(event) { if (event.target.localName == "textbox") event.preventDefault(); } function init() { var container = document.getElementById("container"); container.addEventListener("contextmenu", checkContextMenu, true); }
関数 checkContextMenu
はコンテキストメニューのターゲットが textbox かどうかをチェックし、もしそうならば、preventDefault メソッドを使ってイベントをキャンセルします。このメソッドはコンテキストメニューを無効にする効果があります。関数 init
の中で addEventListener メソッドを使ってイベントリスナが取り付けられています。この関数は load イベントなどのウィンドウの初期化中に呼び出すのがいいでしょう。
コンテキストメニューを取り付ける
コンテキストメニューを要素に取り付けるには context
属性を使います。この属性はどんな XUL 要素にも適用できます。context 属性の値は同じドキュメントにあるコンテキストメニューの id でなければなりません。
この例では、コンテキストメニューがグリッドに取り付けられています。
<menupopup id="insdel-menu"> <menuitem label="Insert"/> <menuitem label="Delete"/> </menupopup> <grid context="insdel-menu"> <columns> <column/> <column flex="1"/> </columns> <rows id="rows"> <row align="center"> <label value="Name:"/> <textbox/> </row> </rows> </grid>
コンテキストメニューを適用したい要素に context
属性を設定すれば、同じコンテキストメニューを複数の要素に取り付けることも出来ます。
ドキュメントエリア全体にコンテキストメニューを取り付けるのも一つの手法です。例えば、Firefox ブラウザでは Web ページを表示するブラウザエリアにただ 1 つのコンテキストメニューを適用しています。次の例は、browser 要素に「contentAreaContextMenu」という id のコンテキストメニューを取り付けます。
<browser context="contentAreaContextMenu">
デフォルトアイテムを示す
いくつかのプラットフォームでは、コンテキストメニューのうち 1 つのアイテムがデフォルトの操作であることが明示されます。一般的には、そのメニューアイテムのラベルが太字で表示されます。どのアイテムがデフォルトアイテムかという事に特に決まりはありませんが、通常は、コンテキストメニューを出さずに普通に左クリックされた時に実行される操作を行うアイテムがデフォルトアイテムとされます。もしそれに該当するアイテムがあれば、default
属性を使ってそれをデフォルトアイテムにする事ができます。
<menupopup id="link-menu"> <menuitem label="Open Link" default="true"/> <menuitem label="Open In New Window"/> <menuitem label="Properties"/> </menupopup>
1 つめのメニューアイテム (Open Link) の default 属性が true に設定されています。対応するプラットフォームでは、これがデフォルトアイテムであることが示されます。他のプラットフォームではこの属性は無視され、メニューアイテムは普通に表示されます。当然ながら、デフォルトアイテムはコンテキストメニュー毎に 1 つでなければ意味がありません。
default 属性はアイテムの表示方法に影響するだけで、それ自体が何かの機能を実行するわけではありません。デフォルトの操作を実行させるためにその要素にコードを関連付けなければならないことに変わりは無いので注意してください。
子要素としてのコンテキストメニュー
要素にコンテキストメニューを関連付ける時、id によって menupopup を参照するよりも、その要素の子要素として menupopup を設置した方が便利な事があります。この方法はコンテキストメニューを 1 つの要素だけに結びつけるときに使う事ができます。また、XBL バインディングの中で id によってポップアップを参照するのは望ましくないでしょうから、この方法を取ると便利です。context
属性が特殊な値である「_child」に設定されると、与えられた id を持つ menupopup を探す代わりに、要素の子要素の中からコンテキストメニューが探されます。例えば、
<vbox context="_child"> <label value="Hello"/> <menupopup> <menuitem label="Cut"/> <menuitem label="Copy"/> <menuitem label="Paste"/> </menupopup> </vbox>
この例では、context 属性が「_child」に設定されているので、 vbox の子要素がコンテキストメニューとして採用されます。この特殊な値は、「child」という id と区別するためにアンダースコアから始めなければなりません。menupopup は例にあるように要素の最後の子要素である必要はありませんが、普通はここに置いた方が便利でしょう。
コンテキストによるメニューアイテムの表示非表示
コンテキストメニューが開かれる時、ポップアップが表示される前に popupshowing イベントが発生します。このイベントはメニュー上で表示されるアイテムを変更するのに使用されます。どのアイテムが表示されるかは、一般的にはコンテキストが何かによって異なります。例えば、画像の上で右クリックをすれば画像に関係するアイテムが表示され、リンクの上でクリックすればリンクに関係するアイテムが表示されるでしょう。
多くの場合、考えられる全てのアイテムを格納した 1 つのコンテキストメニューを使って、必要に応じてアイテムを表示したり隠したりするのが便利でしょう。
アイテムを非表示にするには、そのアイテムの hidden
プロパティを true に設定します。アイテムを表示するには、hidden プロパティを false に設定します。
<script> function showHideDeleteItem() { var deleteItem = document.getElementById("delete"); var rows = document.getElementById("rows"); deleteItem.hidden = (rows.childNodes.length == 0); } </script> <menupopup id="inssel-menu" onpopupshowing="showHideDeleteItem()"> <menuitem label="Insert"/> <menuitem id="delete" label="Delete"/> </menupopup>
この例では、popupshowing イベントが発生すると関数 showHideDeleteItem が呼ばれます。この関数は「rows」という id の要素が子要素を持っているかを調べ、もし持っていなければ、「delete」メニューアイテムの hidden
プロパティを変更して非表示にします。ユーザがコンテキストメニューを開こうとすると、delete メニューアイテムが子要素の数によって表示されるか隠されます。
メニューアイテムを隠す代わりに、disabled
プロパティを設定して無効にするという方法もあります。
deleteItem.disabled = (rows.childNodes.length == 0);
この例では、disabled
プロパティを使った方が適切です。クリックされた要素に絶対に適用できないようなメニューは非表示にするべきですが、特定の状況にあるために適用できないようなアイテムは無効にするべきです。
コンテキストとして何がクリックされたか判定する
コンテキストメニューはマウスだけではなく他の方法でも開かれ得るという事を覚えておく事は重要です。しかしながら、キーボードを使って開かれた場合でも、メニューが適用されるノード (コンテキスト) はあります。それは現在フォーカスのある要素です。マウスを使った場合には、コンテキストはクリックされた要素です。
この要素は document の popupNode プロパティを使って取得できます。次の例では、browser に取り付けられたポップアップで、popupNode プロパティをチェックして画像がクリックされたかどうかを判定しています。
<script> function showHideItems() { var element = document.popupNode; var isImage = (element instanceof Components.interfaces.nsIImageLoadingContent && element.currentURI); document.getElementById("enlarge").hidden = !isImage; document.getElementById("details").hidden = !isImage; } </script> <menupopup id="contentAreaContextMenu" onpopupshowing="showHideItems()"> <menuitem label="Copy"/> <menuitem id="enlarge" label="Enlarge Image"/> <menuitem id="details" label="Image Details"/> </menupopup> <browser src="https://www.mozilla.org" context="contentAreaContextMenu"/>
popupshowing イベントが発生すると、関数 showHideItems
が呼ばれます。popupNode を取得し、それが画像かどうかを調べます。nsIImageLoadingContent インターフェイスは全ての種類の画像に実装されています。画像をサポートするタグにはいくつかの種類があるので、タグを比較するよりもこちらを使った方が便利です。またこの判定ではその要素に URI が設定されているかどうかも確かめています。
最後に、コンテキストが画像かどうかによって 2 つのメニューアイテムの表示非表示が決定されます。結果として、画像の上でコンテキストメニューを開くと 3 つのアイテムが表示され、その他の種類の要素上ではアイテムが 1 つだけ表示されます。