IE 6/7 で文書間通信を実現するための一案2011年12月08日 23時32分

HTML5-WEST.jp 飲み会 UST というのがあるそうで、「参戦希望者募集」とお誘いを受けたのですが、「参戦」というからには何かしら戦の準備を整えねばなるまいと、以前から考えていたことを夜なべして実装しました。

いわゆる HTML5 の文書間通信、window.postMessage() を IE 6/7 でどう再現するかという話で、about:blank を指す隠しフレームを二つ用い、window.name を介することで双方向の通信を実現しています。

どうも IE での about:blank は、その空白ページを読み込ませた文書の生成元を継承するらしく、たとえば http://example.org/ から location.href = 'about:blank'; を実行すれば http://example.org を生成元とする about:blank になります (少なくともそのように見えます)。これを利用し、一つのフレームに二つの文書から交互に about:blank を読み込ませあうことで、そのフレームの window.name を両文書で共有できるという仕組みです。

about:blank を読み込ませるのに location.reload() を使えば履歴に余分な項目を残すこともありませんし、フラグメント識別子を使った場合と違ってデータ量にもだいぶ余裕が出ます。

chrome URL からファイル一覧を取得する2011年01月12日 01時41分

Firefox にて、ディレクトリを指す chrome URL から、そのディレクトリ以下の全ファイルの URL を返すサンプルコードを書きました。

ChromeFiles.get("chrome://browser/content/");
/* => [ "chrome://browser/content/NetworkPanel.xhtml",
 *      "chrome://browser/content/aboutDialog.css",
 *      ...,
 *      "chrome://browser/content/browser.css",
 *      "chrome://browser/content/browser.js",
 *      "chrome://browser/content/browser.xul",
 *      ... ]
 */

特徴として、実際のファイルが (.jar または .xpi に) パッケージ化されているかどうかに関わらず、ファイル一覧を取得できることが挙げられます。ソースコード全体は上記 Gist へのリンクを参照してもらうとして、以下は各関数の解説です。

var ChromeFiles = {
    get: function CF_get(spec) {
        const ios = Cc['@mozilla.org/network/io-service;1'].
                    getService(Ci.nsIIOService);
        let uri = ios.newURI(spec, null, null);
        return this.getByURI(uri);
    },

    ...
};

単に、文字列として受け取った URL を nsIURI のインスタンスにして、getByURI() へ処理を委譲しているだけです。

getByURI: function CF_getByURI(uri) {
    // 1. ディレクトリを指す URL にする
    let baseURI = uri.clone().QueryInterface(Ci.nsIURL);
    baseURI.path = baseURI.directory;

    // 2. chrome URL からローカルファイルシステムでの URL へ変換
    const registry = Cc['@mozilla.org/chrome/chrome-registry;1'].
                     getService(Ci.nsIChromeRegistry);
    let localURI = registry.convertChromeURL(baseURI);

    // 3. ディレクトリ中のファイル名を取得
    let leafNames = null;
    if (localURI instanceof Ci.nsIFileURL) {
        leafNames = this.getLeafNamesByDirectory(localURI.file);
    } else if (localURI instanceof Ci.nsIJARURI) {
        leafNames = this.getLeafNamesByJARURI(localURI);
    } else {
        throw new Error('Unknown URI: ' + localURI.spec);
    }

    // 4. ディレクトリのパスとファイル名を結合
    let baseSpec = baseURI.spec;
    return leafNames.sort().map(function (leafName) baseSpec + leafName);
},
  1. chrome URL において、chrome://{package}/content/chrome://{package}/content/{package}.xul同じリソースを表しnsIIOService#newURI() の引数に前者を渡しても返ってくる URI オブジェクトの spec は後者になります。確実にディレクトリを指す URI オブジェクトを得るためには、自分で URI オブジェクトのプロパティを変更しなければなりません。

    ところが、newURI() で作られたオブジェクトは可変でない (immutable な) ことがあり、このときプロパティに値を設定しようとすると例外が発生します。chrome URL の場合 clone() で生成した URI オブジェクトは可変になるので、まずは URI オブジェクトを複製します。

    パスからディレクトリ部分だけを抜き出すのは、nsIURL インターフェースの directory プロパティを使うのが簡単です。nsIURL インターフェースを経由すれば、ディレクトリ以外にもファイル名や拡張子などをすぐに取得できます。

  2. chrome URL からローカルファイルシステム上でのファイル位置をあらわす URL への変換は、nsIChromeRegistry#convertChromeURL() で一発です。これにより得られる URL は大抵の場合 file URL か jar URL かのいずれかです。

  3. ディレクトリ中のファイル名一覧を配列として取得します。処理本体は file URL の場合と jar URL の場合で別になります。

    なお、QueryInterface() を使わなくとも、instanceof 演算子で nsIFileURL インターフェースを実装していることを確認できたなら、それ以降は nsIFileURLfile プロパティから nsIFile オブジェクトを取得できます。

  4. 得られたファイル名一覧の順序はわからないので、辞書順で並べ替えます。その後にディレクトリ部分を表す chrome URL と結合すれば、ディレクトリ直下のファイルを指す chrome URL の一覧が得られます。

getLeafNamesByDirectory: function CF_getLeafNamesByDirectory(dir) {
    let files = dir.directoryEntries;
    let leafNames = [];
    while (files.hasMoreElements()) {
        let file = files.getNext().QueryInterface(Ci.nsIFile);
        if (file.isFile())
            leafNames.push(file.leafName);
    }
    return leafNames;
},

ディレクトリを表す nsIFile オブジェクトから、その子ファイルの名前一覧を取得します。単に子ファイルを列挙していき、それがディレクトリなどでないときにファイル名を取得するだけです。

getLeafNamesByJARURI: function CF_getLeafNamesByJARURI(jarURI) {
    // 1. ZipReader を作成
    let zip = this.openZipReader(jarURI.JARFile);

    try {
        // 2. ディレクトリ直下のファイルのパスを取得
        let baseEntry = jarURI.JAREntry;
        let pattern = baseEntry + '?*~' + baseEntry + '?*/*';
        let entries = zip.findEntries(pattern);

        // 3. ファイル名部分だけを抜き出し、返す
        let leafNames = [];
        while (entries.hasMore())
            leafNames.push(entries.getNext().substring(baseEntry.length));
        return leafNames;
    } finally {
        zip.close();
    }
},

jar URI は jar:file://path/to/file.jar!/path/to/entry/ のような形で表されます。nsIJARURI インターフェースは、file://path/to/file.jar の部分を示す JARFile プロパティ (返ってくるのは nsIURI オブジェクト) と、/path/to/entry/ の部分を指す JAREntry プロパティ (返ってくるのは文字列) を持っています。

  1. JAR ファイル (または XPI ファイル) は ZIP 書庫なので、内部のファイル情報を読み取るためには nsIZipReader のインスタンスを作成し書庫を開く必要があります。

  2. 書庫内部のファイル名一覧を取得するには findEntries() を使います。ここで指定するファイル名のパターンにおいて、"?" は任意の1文字を、"*" は任意の文字列を、"pattern1~pattern2"pattern1 にマッチするが pattern2 にはマッチしないものを表します。

    "/path/to/entry/*" というパターンでは /path/to/entry/ 自信も含まれてしまうので、ディレクトリではないファイルだけを抽出するために "/path/to/entry/?*" と指定します。また、それだけだと子孫ディレクトリ中のファイルも含まれるので、"/path/to/entry/?*/*" を除外してやります。

  3. findEntries() で得られた値にはディレクトリ部分も含まれるので、その部分は切り取ってファイル名だけにします。

openZipReader: function CF_openZipReader(uri) {
    let zip = Cc['@mozilla.org/libjar/zip-reader;1'].
              createInstance(Ci.nsIZipReader);
    if (uri instanceof Ci.nsIFileURL) {
        // 1. file URL なら単にそのファイルを開く
        zip.open(uri.file);
    } else if (uri instanceof Ci.nsIJARURI) {
        // 2. jar URL なら JAR ファイル内部のファイルを開く
        let innerZip = this.openZipReader(uri.JARFile);
        zip.openInner(innerZip, uri.JAREntry);
    } else {
        throw new Error('Unknown URI: ' + uri.spec);
    }
    return zip;
},
  1. JAR ファイルがローカルファイルシステム上に直接存在するなら、単に open() メソッドにファイルオブジェクトを渡して開くだけです。

  2. 開こうとする JAR ファイルが別の書庫内に存在することもあります。その場合は openInner() に、JAR ファイルが含まれる書庫と、その書庫内での JAR ファイルのパスを指定してやります。

    nsIZipReader#openInner() は Firfox 4 で追加されたものですが、Firefox 4 より前では jar URL がネストすることはないといっていいので、ここで使っても問題ないでしょう。

たとえば Firefox 4 Beta でツリー型タブを使うと、chrome://treestyletab/content/ の実体は jar:jar:file://{profile}/extensions/treestyletab@piro.sakura.ne.jp.xpi!/chrome/treestyletab.jar!/content/treestyletab/ といった URL になりますが、上記のようにすればその内部のファイル構成を知ることができます。

また、resource://gre/modules/XPCOMUtils.jsm といった resource URL に関しても、nsIResProtocolHandler#resolveURI() を使えばローカルファイルシステム上の URL へ変換でき、上と同様にファイル一覧の取得などが可能になります。

リンクのようなボタンを作る2010年12月15日 23時52分

こんばんは、JavaScript Advent Calendar 2010、15 日目担当の nanto_vi (なんと) です。12 月 15 日が何の日か調べてみると東北本線が宮城県に到達した日とのこと。当時は上野から仙台まで 12 時間 20 分かかったそうです。それから 123 年を経た現在では同じ時間で鹿児島中央から新青森まで行けるようになり、鉄道の速度にも JavaScript の実行速度にも日進月歩を感じる今日この頃です。

さて、アプリケーションを作っていると、見た目はリンクのようだがリンクでない UI 部品を使いたくなるときがあります。ここで「リンクでない」とは、クリックしてもページ遷移が発生しないということです。このような UI 部品は、ページ遷移の代わりにメニューの表示といった何らかのアクションを引き起こす、すなわちボタンとして振舞います。

ユーザーインターフェース記述言語として HTML を使っているとき、この「リンクのようなボタン」をどのように実現すればいいのでしょうか。

input、button 要素

ボタンとして振舞うものはボタンとして記述すべきです。HTML では汎用的なボタンとして <input type="button"> 及び <button type="button"> が用意されています。スタイルシートを使えば見た目をリンクのようにもできるでしょうし、画像を使いたければ <input type="image"> もあります。

<input type="button" value="リンクのようなボタン" onclick="...">
<button type="button" onclick="...">リンクのようなボタン</button>

しかし、フォームコントロールに対してはスタイルシートが期待通り適用されないこともあり、ボタンをどうしてもインライン要素にしたいときにこの方法は取れません。

a 要素

リンクのように見えるならリンクにすればいいということで a 要素が使われることもあります。click イベントを処理するときにデフォルトアクションをキャンセルすれば、もともと指定してあったリンク先に飛ぶことはありません。あるいはリンク先に javascript:void(0); と指定することでページ遷移が発生しないようにします。

<a href="#" onclick="...; return false;">リンクのようなボタン</a>

しかし、a 要素はあくまでハイパーリンク、すなわち他のリソースへ移動するためのものです。外見がどうあろうとリンクでないものの記述に使うの好ましくありません。

span 要素

click イベントに対する処理はどんな要素にも付加できるので、a 要素にこだわる必要はありません。「リンクのような」の部分はスタイルシートで実現できます。

<style>
.trigger {
  color: #00f;
  text-decoration: underline;
  cursor: pointer;
}
</style>

<span class="trigger" onclick="...">リンクのようなボタン</span>

しかし、これではキーボードを用いてボタンにアクセスすることができません。多くのブラウザでは Tab キーでリンクやフォームコントロールへ移動できますが、span 要素はその対象から外れています。

span 要素 + tabindex 属性

tabindex 属性はフォーカス順を変更するものだと思っていたあなた、それはこの属性が持つパワーのほんの一部でしかありません。HTML5 において tabindex 属性は要素をフォーカス可能にする属性として生まれ変わったのです。この属性はどんな要素にもつけられ、値に 0 を指定すればその要素がキーボードアクセス可能になります。

<span class="trigger" tabindex="0" onclick="...">
  リンクのようなボタン
</span>

しかし、Tab キーでこのボタンにフォーカスし、Enter キーを押しても何もおきません。a要素によるリンクなら Enter キーを押すと click イベントが発生するのにも関わらずです。なお、Opera なら span 要素でも Enter キーにより click イベントが発生し、指定したアクションが実行されます。

span 要素 + tabindex 属性 + onkeypress 属性

click イベントが発生しないなら自分で click イベントに対する処理を呼び出せばいいのです。onclick 属性に指定したコードは onclick プロパティから関数として取得できます。Enter キーを表すキーコードは 13 なので、そのときのみ処理を実行します。

<span class="trigger" tabindex="0" onclick="..."
      onkeypress="if (event.keyCode === 13) this.onclick(event);">
  リンクのようなボタン
</span>

しかし、click イベントに対する処理が onclick 属性に書かれているとは限りません。addEventListener や attachEvent メソッドでイベントリスナが追加されていることもあれば、祖先要素、文書ノードで click イベントが処理されていることもあります。また、Opera ではこの場合 Enter キーを押すと onclick 属性の内容が2回実行されてしまいます。

span 要素 + tabindex 属性 + キーイベント処理

click イベントに対する処理を直接実行できなくとも、click イベントを発生させれば自然とそれらが実行されます。イベントを発生させるのに、DOM イベントモデルでは dispatchEvent、IE のイベントモデルでは fireEvent メソッドを用います。Opera 対策に keypress イベントのデフォルトアクションをキャンセルしておきましょう。

<script>
function activate(event) {
    event = event || window.event;
    if (event.keyCode !== 13) return;
    if (document.createEvent) {
        var e = document.createEvent('MouseEvent');
        e.initMouseEvent(
            'click', true, true, event.view, 1,
            event.screenX, event.screenY, event.clientX, event.clientY,
            event.ctrlKey, event.altKey, event.shiftKey, event.metaKey, 0, null
        );
        event.target.dispatchEvent(e);
        event.preventDefault();
    } else if (document.createEventObject) {
        var e = document.createEventObject(event);
        event.srcElement.fireEvent('onclick', e);
        event.returnValue = false;
    }
}
</script>

<span class="trigger" tabindex="0"
      onclick="..." onkeypress="activate(event);">
  リンクのようなボタン
</span>

しかし、視覚的なブラウザであれば見た目からこの要素が何らかのアクションを引き起こすことがわかりますが、環境によってもそもそもこれが「押せる」ということすら伝わらないかもしれません。

span 要素 + tabindex 属性 + キーイベント処理 + role 属性

視覚によらず、機械的に UI 部品を認識するための仕様として WAI-ARIA があります。要素がボタンの「役割」を果たすことを表すには、role 属性の値に "button" を指定します。これで人の目からだけでなく、機械から見たときもボタンとして認識・操作できるようになりました。

<span class="trigger" role="button" tabindex="0"
      onclick="..." onkeypress="activate(event);">
  リンクのようなボタン
</span>

サンプル

リンクのようなボタンのサンプルで実際の挙動を確認できます。

終わりに

以上はあくまで JavaScript の使える環境が前提です。スクリプトが動かなくとも最低限の機能は利用できるよう気をつけましょう。スクリプトが動く場合でも、本当にリンクのようなボタンでなければいけないのか、通常のボタン、またはハイパーリンクでは実現できないのかよく検討した上で使うようにしてください。

参考資料

Kanasan.JS JavaScript 第 5 版読書会 #10 XPath 発表資料2010年09月27日 08時58分

6 月のことになりますが、Kanasan.JS JavaScript 第 5 版読書会 #10 にて XPath に関する発表を行いました。内容は基本的な XPath の解説です。

本番では図の準備が間に合わずホワイトボードに描きながら発表したのですが、逐次挙がる質問に対してもすぐ描き直して対応できたので、かえって理解を深めるのに役立ったのではないかと思います。資料中の注釈は口頭で説明したことなどを書き起こしたものです。

タイトルに excellent と入っていますが何か特に素晴しいということはなく、単に頭韻を踏みたかっただけです。(しかし excellentexpression では ex の発音が違うので踏めてないという……)

Mozilla 勉強会 @ 大阪2010年03月03日 04時53分

2 月 20 日に行われた Mozilla 勉強会 @ 大阪に行ってきました。「js-ctypes で音声読み上げ」という題でライトニングトークをしたので、そのスライドとサンプルコードを公開します。

js-ctypes は C/C++ 用のライブラリ関数を JavaScript から呼び出すための機能で、Firefox の次期バージョンに搭載される予定です。ここでは「AquesTalkとkakasiを組み合わせて再生。 - 世界はアルゴリズムでできている。」を参考に音声読み上げライブラリを呼び出してみました。

上記コードでは標準 C ライブラリ関数を使って無理やりポインタを扱っていますが、Overview of js-ctypesMozillaWiki の js-ctypes のページを見るに、将来的には標準でポインタや構造体、コールバック関数を使えるようにするみたいです。

IME の実装の概要について (中野雅之さん)

現在 Mozilla で IME 周りのコードを書いているのは中野さんだけだそうです (レビュワは Windows と Linux に一人ずつ)。「TSF を使う (1) - Windows Input Method の歴史 - NyaRuRuの日記」によると Mozilla は IME 関係の描画を自前で行っているそうで、それを一人でやり続けているというのには頭が下がります。

研究プラットフォームとしての Firefox の可能性 (山本岳洋さん)

研究の一環として Rerank.jp を作っている方です。当初は C# でアプリケーションを作成していたが、いちいち起動するのが面倒で自分でさえも使わなくなってしまったとのこと。そういう点では、常に起動しているアプリケーション実行環境で、おまけにクロスプラットフォームというのはとっつきやすいのかもしれませんね。

ちなみに私が知る限りでは、ほかにも次のような研究が Firefox (Mozilla) を使っています。

Jetpack Reboot (あかつかだいすけさん)

今 Jetpack は基礎から作り直している最中だそうです。Jetpack のランタイムを各フィーチャーが含んで、個別の「拡張機能」として扱えるようになり、またそれに伴い addons.mozilla.org (AMO) での公開手順も整備中とのこと。ある程度広まって問題点も明らかになり、それでいて 1.0 も出ていないこの時期は、まさに「再起動」にぴったりだと思います。

modest と研究と勉強会

Mozilla/Firefox の開発情報を集約する場として、Mozilla Developer Street (modest) を積極的に使ってほしいという話がありました。Firefox を研究に使うのと同時に、modest を大学で使っていくのもありだそうです。

また、このような勉強会を今後も定期的に開いていくそうですが、大学での開催も可能とのことです。Firefox を研究に使いたいけれど開発方法がわからないという方は、modest で声を上げてみてはいかがでしょうか。