ポップアップメニューを作る時、各項目の表示・非表示を場合に応じて変えることはよくありますが、この時にセパレータの扱いで頭を悩ませることがあります。
例えば、以下のようなメニューを考えてみましょう。
クラス名の意味は以下の通りです。このクラス名を処理に使うとは限りませんが、コードを見やすくするための便宜上のものということでご理解ください。
さて。このそれぞれの「場合」に応じて必要なアイテムだけを表示するというのは、よくある話です。例えば「日本語の文字列を選択した場合」はどうなるでしょうか。
非表示になった項目を隠してみると、見て分かるとおり、メニューの先頭や最後にセパレータが来ていたり、セパレータだけが連続して表示されていたりといったことが起こっているのが分かります。上の例ではそういう「無駄なセパレータ」を強調してありますが、これらを非表示にするにはどんな方法があるでしょうか?
まず思いつくのは、各アイテムの表示・非表示を切り替えるのと同時に、「そのセパレータより上の(下の)項目すべてが非表示なら、そのセパレータも非表示にする」という方法です。しかしこれは、判断しなくてはならない「場合」の数が増えてくると手に負えなくなってきますし、メニューの最初や最後にセパレータが表示されてしまうような場合を防ぐためにも、またややこしい条件判断が必要になってきます。
次に思いつくのは、一旦アイテムの表示・非表示を切り替えた上で、改めてセパレータだけを調べて表示・非表示を切り替えるという方法です。これなら難しい条件判断は必要ありません。しかし、メニューの内容を更新するたびに何度もループを回さなくてはならないというデメリットもあります。
「メニューの先頭・最後にあるセパレータや、連続するセパレータの一つ目以外」という条件を満たすセパレータだけを取得して、それらだけを非表示にする。これが、実現したい処理の内容の要点です。この条件を表す何らかの指定を使ってノードを一発取得することができれば、話は早いですよね。
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が結びつけられているものとしています。