WebExtensionsによるFirefox用の拡張機能でメニューやパネル風のUIを提供するライブラリ:MenuUI.js


(この記事は、Firefoxの従来型アドオン(XULアドオン)の開発経験がある人向けに、WebExtensionsでの拡張機能開発でのノウハウを紹介する物で、所属会社のブログの2018年5月11日の記事の再掲です。)

XULでは<menupopup>という要素の中に<menuitem><menu>などの要素を置いてネイティブアプリと同じ様式の階層型メニューを実装できました。それに対し、WebExtensionsベースの拡張機能ではUIは基本的にHTMLで作成します。この時に悩み所になるのが、メニューをどうやって実装するかという点です。

HTMLにもmenu要素という物がありますが、これはXULのメニュー関連要素の完全な代替とはなりません。ボタンに対してのポップアップメニューやツールバー上のメニューは本稿執筆時点でFirefoxでは実装されておらず、唯一使用できるコンテキストメニューでの用法についても、専用のコンテキストメニューを実現する物ではなく、あくまで「Webページ上のコンテキストメニューに追加の項目を設ける」という性質の物です。

Firefoxのコンテキストメニューとは切り離された別のメニューを実現するには、今のところHTML(+JavaScript+CSS)でそれらしい見た目と振る舞いのUIを独自に実装するほかありません。実装例自体は既存の物が多数あり、React用Vue用jQuery用など、採用するフレームワークに合わせて選べます。しかし、作りたいアドオンの性質上、フレームワークの導入には気乗りしないという人もいるのではないかと思います。

そこで、このような場面で使える軽量なライブラリとしてMenuUI.jsという物を開発しました。

基本的な使い方

このライブラリは、HTMLファイル中に静的に記述された物か動的に生成された物かを問わず、<ul><li>で構成された入れ子のリストを階層型のメニューとして振る舞わせるという物です。表示例はこんな感じです。

このライブラリを使ってメニュー風UIを提供するには、まず任意のHTMLファイル内でMenuUI.jsを読み込みます。

<script type="application/javascript" src="./MenuUI.js"></script>

また、それと併せてメニューとして表示するためのリストを以下の要領で用意します。

<ul id="menu">
  <li id="menu-save">保存(&amp;S)</li>
  <li>コピー(&amp;P)
    <ul>
      <li id="menu-copy-title">タイトル(&amp;T)</li>
      <li id="menu-copy-url">&amp;URL</li>
      <li>メタデータ(&amp;M)
        <ul>
          <li id="menu-copy-author">作者名(&amp;A)</li>
          <li id="menu-copy-email">&amp;Eメール</li>
        </ul>
      </li>
    </ul>
  </li>
  <li class="separator"></li>
  <li id="menu-close">閉じる(&amp;C)</li>
</ul>
  • サブメニューを定義したい場合はリストを入れ子にして下さい。
  • この例では、後々どの項目が選択されたかを容易に判別できるようにするために、コマンドとして実行されうる項目にIDを割り当てています。(通例、サブメニューを持つ項目はそれ自体が選択されてもコマンドとしては実行されないため、それらにはIDを割り当てていません。)
  • リストの内容となるテキストがそのままメニュー項目のラベルになります。
    • この時、ラベルの一部に&(静的に記述する場合は実体参照の&amp;)を書いておくと、その次の文字がメニュー項目のアクセスキーになります。

次に、このリストをメニューとして初期化します。以下の要領でMenuUIクラスのインスタンスを作成します。

var menuUI = new MenuUI({
  root: document.getElementById('menu'), // メニューにするリストの最上位の<ul>を指定
  onCommand: (aItem, aEvent) => {
    // 項目が選択された時に実行されるコールバック。
    // ここでは項目のidで処理を振り分けるようにしている。
    switch (aItem.id) {
      case 'menu-save':
        doSave();
        return;
      case 'menu-copy-title':
        doCopy('title');
        return;
      ...
    }
  }
});

作成されたインスタンスは、open()というメソッドを実行するとメニューとして表示されます。ページ上を右クリックした際にブラウザ本来のコンテキストメニューの代わりとして表示したい場合であれば、以下のようにします。

window.addEventListener('contextmenu', aEvent => {
  aEvent.stopPropagation();
  aEvent.preventDefault();
  menuUI.open({
    left: aEvent.clientX,
    top:  aEvent.clientY
  });
});

また、何かのボタンをクリックした時にドロップダウンメニューのように表示させたい場合には、以下のようにボタンのクリック操作を監視した上で、そのボタンなどの要素をopen()メソッドの引数にanchorというプロパティで指定すると、ボタンの位置に合わせてメニューが表示されます。

const button = document.getElementById('button');
button.addEventListener('click', () => {
  menuUI.open({
    anchor: button
  });
});

コンストラクタのオプションやopen()のオプションの詳細な内容については、リポジトリ内のREADMEファイルを参照して下さい。

キーボードでの操作への対応

このライブラリの特長として、キーボード操作への対応に力を入れているという点が挙げられます。以下は、現時点で対応している代表的な操作です。

  • ↑↓カーソルキーで項目のフォーカスを移動
  • →カーソルキーでサブメニューを開く
  • ←カーソルキーでサブメニューを閉じる
  • Escキーでメニューを閉じる
  • Enterキーでフォーカスしている項目を選択(コマンドを実行)
  • メニューのラベルから検出されたアクセスキーで項目を選択(同じアクセスキーの項目が複数ある場合はフォーカス移動のみ)

メニューを開くまではポインティングデバイスを使うが、その後の操作はキーボードで行う、というような場面は意外とあります。既存のライブラリはキーボード操作に対応していなかったり、操作体系が一般的なGUIのメニューではなくWebページ上のリンクに準拠していたりと、「ネイティブアプリのGUIと同じ感覚で使える」事が特長だったXULからの移行においてストレスになる部分があったため、この点にはとりわけ気をつけて開発しています。

まとめ

XULアドオンでのカスタムメニューの代替となる軽量ライブラリであるMenuUI.jsについて、その使い方を解説しました。「以後確認しない」のようなチェックボックスを伴った確認ダイアログを表示するRichConfirm.jsと併せて、Firefox用アドオンの開発にご活用いただければ幸いです。