X-0033 メニューのセパレータの表示・非表示を効率よく制御する

解決したい問題

ポップアップメニューを作る時、各項目の表示・非表示を場合に応じて変えることはよくありますが、この時にセパレータの扱いで頭を悩ませることがあります。

例えば、以下のようなメニューを考えてみましょう。

  • <menuitem label="リンクをタブで開く" class="link"/>
  • <menuitem label="リンクをウィンドウで開く" class="link"/>
  • <menuseparator>
  • <menu label="選択範囲を送る" class="selection"> <menupopup>
    • <menuitem label="Google検索" class="selection"/>
    • <menuitem label="Yahoo!検索" class="selection"/>
    • <menuseparator>
    • <menuitem label="英辞郎" class="selection english"/>
    • <menuitem label="Excite翻訳" class="selection english"/>
    </menupopup> </menu>
  • <menuseparator>
  • <menuitem label="切り取り" class="selection"/>
  • <menuitem label="コピー" class="selection"/>
  • <menuitem label="貼り付け" class="not-link"/>
  • <menuseparator>
  • <menuitem label="削除" class="selection"/>
  • <menuseparator>
  • <menuitem label="すべて選択" class="not-link not-selection"/>
  • <menuseparator>
  • <menuitem label="プロパティ"/>

クラス名の意味は以下の通りです。このクラス名を処理に使うとは限りませんが、コードを見やすくするための便宜上のものということでご理解ください。

link
リンクの上でメニューを開いた時に表示
not-link
リンク以外の上でメニューを開いた時に表示
selection
文字列を選択している時に表示
not-selection
文字列を選択していない時に表示
english
英語の文字列を選択している時に表示

さて。このそれぞれの「場合」に応じて必要なアイテムだけを表示するというのは、よくある話です。例えば「日本語の文字列を選択した場合」はどうなるでしょうか。

  • <menuseparator>
  • <menu label="選択範囲を送る" class="selection"> <menupopup>
    • <menuitem label="Google検索" class="selection"/>
    • <menuitem label="Yahoo!検索" class="selection"/>
    • <menuseparator>
    </menupopup> </menu>
  • <menuseparator>
  • <menuitem label="切り取り" class="selection"/>
  • <menuitem label="コピー" class="selection"/>
  • <menuitem label="貼り付け" class="not-link"/>
  • <menuseparator>
  • <menuitem label="削除" class="selection"/>
  • <menuseparator>
  • <menuseparator>
  • <menuitem label="プロパティ"/>

非表示になった項目を隠してみると、見て分かるとおり、メニューの先頭や最後にセパレータが来ていたり、セパレータだけが連続して表示されていたりといったことが起こっているのが分かります。上の例ではそういう「無駄なセパレータ」を強調してありますが、これらを非表示にするにはどんな方法があるでしょうか?

まず思いつくのは、各アイテムの表示・非表示を切り替えるのと同時に、「そのセパレータより上の(下の)項目すべてが非表示なら、そのセパレータも非表示にする」という方法です。しかしこれは、判断しなくてはならない「場合」の数が増えてくると手に負えなくなってきますし、メニューの最初や最後にセパレータが表示されてしまうような場合を防ぐためにも、またややこしい条件判断が必要になってきます。

次に思いつくのは、一旦アイテムの表示・非表示を切り替えた上で、改めてセパレータだけを調べて表示・非表示を切り替えるという方法です。これなら難しい条件判断は必要ありません。しかし、メニューの内容を更新するたびに何度もループを回さなくてはならないというデメリットもあります。

XPathで解決する方法

「メニューの先頭・最後にあるセパレータや、連続するセパレータの一つ目以外」という条件を満たすセパレータだけを取得して、それらだけを非表示にする。これが、実現したい処理の内容の要点です。この条件を表す何らかの指定を使ってノードを一発取得することができれば、話は早いですよね。

DOM3 XPathをノードの検索に活用するで定義したgetNodesFromXPath()を使えば、XPath式による表現で、この要求を実現することができます。


var popup = document.getElementById('popup');

var hiddenSeparators = getNodesFromXPath(
       'descendant::xul:menuseparator[not(following-sibling::*[not(@hidden)]) or not(preceding-sibling::*[not(@hidden)]) or local-name(following-sibling::*[not(@hidden)]) = "menuseparator"]',
       popup
   );

for (var i = 0; i < hiddenSeparators.snapshotLength; i++)
    hiddenSeparators.snapshotItem(i).hidden = true;

どうでしょう。XPath式は少々複雑ですが、制御文は非常にシンプルになっていることがお分かり頂けると思います。

なお、「コンテキストノードの子孫のmenuseparator要素で、『表示されている要素』が後続していない、あるいは、『表示されている要素』が先行していない(=そのセパレータが最初かあるいは最後の『表示されている要素』である)もの、または、次の『表示されている要素』のローカル名がmenuseparatorであるもの」。この条件をそのままXPath式にしたものが、 descendant::xul:menuseparator[not(following-sibling::*[not(@hidden)]) or not(preceding-sibling::*[not(@hidden)]) or local-name(following-sibling::*[not(@hidden)]) = "menuseparator"] です。この式は、非表示の要素=hidden属性が指定されている要素、というXULの仕様を利用しています。

ここでは、名前空間接頭辞「xul」にXULの名前空間URIが結びつけられているものとしています。