Now browsing the archives for the 'DOM' category.

nsIEventListenerService でDOMイベントリスナを列挙する

modest に投稿した記事と同内容です。

nsIEventListenerService というXPCOMサービスを使うと、 XUL や HTML ドキュメント内のある要素に対してどんなDOMイベントリスナが追加されているかを調べることができます。以下は、 browser.xul にてFirefoxの「ホーム」ボタンに追加されたイベントリスナをエラーコンソールに列挙するサンプルです。

var els = Cc["@mozilla.org/eventlistenerservice;1"].
          getService(Ci.nsIEventListenerService);
var infos = els.getListenerInfoFor(document.getElementById("home-button"), {});
infos.forEach(function(info) {
    Application.console.log(info.type + " => " + info.toSource());
});

nsIEventListenerService の getListenerInfoFor メソッドは、引数で渡した要素のイベントリスナの情報を、 nsIEventListenerInfo オブジェクトの配列として返します。さらに、各 nsIEventListenerInfo オブジェクトについて、 type プロパティでイベントリスナの種類(click, keypress, mousedown など)を調べたり、 JavaScript のリスナであれば toSource() で内容を文字列化したりできます。ただし、 nsIEventListenerInfo オブジェクトはイベントリスナそのものではないので、 getListenerInfoFor で取得したイベントリスナを removeEventListener で削除する、といったことはできません。あくまでもデバッグ用です。

なお、 nsIEventListenerService は Firefox 3.6 (Gecko 1.9.2) 以降で利用可能です。

TOP

Firefox 3.6 にて HTMLElement.classList が実装

Firefox 3.5 以前

HTML中のある要素のクラス名を取得するには、 HTMLElement.className プロパティを使用する。
クラス名はスペース区切りで複数の値を指定可能であるので、クラス名にある値が含まれるかどうかを判定するには、

var elt = document.getElementById("test");
elt.className.indexOf("foo") >= 0;

のようにしてやればよいが、これでは foobar のような値が含まれている場合も true と判定されてしまう。

Firefox 3.6 以降

Firefox 3.6 にて導入された HTML 5 の仕様の一部である HTMLElement.classList プロパティにより、複数の値が指定されたクラス名の扱いが簡単になる。
HTMLElement.classList プロパティは、 DOMTokenList 型オブジェクトであり、 contains メソッドによってリスト中にある値が含まれるかどうかを正確に調べることができる。

var elt = document.getElementById("test");
elt.classList.contains("foo");

add メソッドや remove メソッドによってクラスに値を追加・削除したり、 toggle メソッドで値の有無を切り替えたりすることも可能。
クラスの個々の値を取得するには以下のようにする。

for (var i = 0; i < elt.classList.length; i++) {
    elt.classList.item(i);
}

TOP

Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その3~ドロップ処理)

その2~ドラッグ処理から引き続き、ドラッグ元をドロップ先へドロップ可能にするための処理と、実際にドロップしたときの処理を追加する。

ドロップ先へのドロップを可能にする

ドロップ先の要素では、テキストボックス (html:input 要素や xul:textbox 要素) などを除き、通常はいかなる形式の転送データでもドロップ不可となっている。ドロップ可能にするには、 dragenter と dragover の2つのイベントハンドラで event.preventDefault() を呼び出す。

はじめに、転送データの形式によらず常にドロップ可能にするには、 handleDropEvents 関数にて以下のような処理を追加する。

        case "dragenter": 
        case "dragover": 
            event.preventDefault();
            break;

別の書き方として、 XUL / HTML のイベントハンドラ側で ondragenter=”return false;” のようにする方法もある。

実際は特定の転送データ形式、例えばリンクのドロップのみを可能にする場合が多いので、 dragenter と dragover の2つのイベントハンドラで転送データの形式を調べ、条件付きで event.preventDefault() を呼び出すようにする。

転送データの形式を調べるには、 DataTransfer オブジェクトの types プロパティを使用する。
ひとまずは、 dragenter と dragover イベント発生時に転送データに含まれるすべての形式を列挙してみる。

        case "dragenter": 
        case "dragover": 
            for (var i = 0; i < event.dataTransfer.types.length; i++) {
                dump("    " + event.dataTransfer.types.item(i) + "
");
            }
            break;

DataTransfer の types プロパティに特定のデータ形式が含まれるかどうかは、以下のように contains メソッドを使用すると良い。

        case "dragenter": 
        case "dragover": 
            if (event.dataTransfer.types.contains("text/url-list") || 
                event.dataTransfer.types.contains("text/plain"))
                event.preventDefault();
            break;

動作確認 (1)

ドラッグ元をドラッグし、マウスの右クリックを放さずにドロップ先の枠内へ入ったり出たりすると dragenter, dragover, dragleave の3つのイベントが発生することを確認してください。
なお、 XUL / HTML をブラウザタブで開いている場合、マウスの右クリックを放してドロップすると、ブラウザタブ側でドロップイベントが発生して URL を開く動作となります。

ドロップされたデータを取得する

次に、 drop イベント発生時にドロップされた転送データを取得する処理を追加する。
ドロップされた転送データを取得するには、まず dragenter, dragover イベントハンドラでやったように DataTransfer オブジェクトの types プロパティから転送データに目的の形式が含まれることをチェックした上で、DataTransfer オブジェクトの getData メソッドで指定した形式の転送データを取得する。
なお、下記のサンプルコードでは、 XUL / HTML をブラウザタブで開いている場合を考慮し、 event.preventDefault() を呼び出して、ブラウザタブ側でドロップイベントが発生するのを阻止している。

        case "drop": 
            event.preventDefault();
            var data = null;
            if (event.dataTransfer.types.contains("text/url-list"))
                data = event.dataTransfer.getData("text/url-list");
            else if (event.dataTransfer.types.contains("text/plain"))
                data = event.dataTransfer.getData("text/plain");
            alert("Dropped URL: " + data);
            break;

以下のように、あらかじめドロップ可能なデータ形式を配列で定義しておいたほうが見通しの良いソースコードとなるかもしれない。

        case "drop": 
            event.preventDefault();
            var data = null;
            var supportedTypes = ["text/url-list", "text/plain"];
            for each (type in supportedTypes) {
                if (event.dataTransfer.types.contains(type)) {
                    data = event.dataTransfer.getData(type);
                    break;
                }
            }
            alert("Dropped URL: " + data);
            break;

動作確認 (2)

ドラッグ元をドラッグしてドロップ先へドロップすると drop イベントが発生することを確認してください。
また、ドロップされたURLがメッセージボックスで表示されることを確認してください。

参考

Drag Operations – MDC

関連記事

Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その1~イベントハンドラの追加)
Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その2~ドラッグ処理)
Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その3~ドロップ処理)

TOP

Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その2~ドラッグ処理)

その1~イベントハンドラの追加から引き続き、ドラッグ元の要素をドラッグ開始した際に、転送データをセットする処理を追加する。

DataTransfer オブジェクト

ドラッグした際に転送データをセットする処理や、ドロップした際に転送データを取得する処理は、 DataTransfer オブジェクト (event.dataTransfer) によって行う。

ドラッグ元をドラッグ開始時、つまり dragstart イベント発生時、転送データをセットするには、 DataTransfer オブジェクトの setData メソッドを使用する。 setData メソッドの第1引数は転送データの形式、第2引数は転送データの値(文字列に限る)である。転送データの形式は、単純な文字列であれば「text/plain」、URL(複数も可)であれば「text/url-list」といった値を用いる。もちろん、一度のドラッグで複数の形式の転送データをセットすることも可能である。

サンプルコード (JavaScript)

handleDragEvents 関数へ以下のような処理を追加する。

        case "dragstart": 
            // 転送データをセットする
            event.dataTransfer.setData("text/url-list", "http://www.mozilla.org/");
            event.dataTransfer.setData("text/plain", "http://www.mozilla.org/");
            break;

動作確認

ドラッグ元をドラッグして dragstart, drag, dragend の3つのイベントが発生することを確認してください。
現段階ではドロップ先の処理が未完ですので、ドラッグ時の転送データが正しくセットされていることを確認するために、 Firefox のロケーションバーなどにドロップするか、メモ帳などの別アプリへドロップして、「http://www.mozilla.org/」という文字列が貼り付けされることを確認してください。

参考

DataTransfer オブジェクトの詳細:
DataTransfer – MDC
nsIDOMDataTransfer.idl

転送データの形式の詳細:
Recommended Drag Types – MDC

関連記事

Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その1~イベントハンドラの追加)
Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その2~ドラッグ処理)
Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その3~ドロップ処理)

TOP

Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その1~イベントハンドラの追加)

Firefox 3.1 (b1pre) にて HTML 5 のドラッグ&ドロップ API が実装され、 HTML の Web アプリ、 XUL の拡張機能のいずれからも同じように利用可能となった。

拡張機能開発者としては、ドラッグ&ドロップに関する各種処理を、 XPCOM サービスの nsIDragService や面倒くさい nsDragAndDrop.js を使わずに、単純な DOM の API のみで記述できるようになったので、非常にありがたい。

ドラッグ&ドロップのイベント

ドラッグ&ドロップ操作時、ドラッグ元の要素とドロップ先の要素において、以下のようなイベントが発生する。

イベント名 (event.type) イベント発生対象 (event.target) イベント発生のタイミング
dragstart ドラッグ元 ドラッグ開始時
drag ドラッグ元 ドラッグ中
dragend ドラッグ元 ドラッグ終了時
dragenter ドロップ先 ドラッグオーバー開始時
dragover ドロップ先 ドラッグオーバー中
dragleave ドロップ先 ドラッグオーバー終了時
drop ドロップ先 ドロップ時

サンプルコード (HTML)

2つの div 要素を配置し、一方をドラッグ元、もう一方をドロップ先とする。
ドラッグ元となる div 要素には、「draggable=”true”」属性をセットしなければならない。ただし、リンク(a 要素)や画像 (img 要素)などは、「draggable=”true”」を指定しなくても自動的にドラッグ可能となる。

ドラッグ元の要素には ondragstart, ondrag, ondragend の3つのイベントハンドラを追加し、ドロップ先の要素には ondragenter, ondragover, ondragleave, ondrop の4つのイベントハンドラを追加する。ただし、必要最低限のドラッグ&ドロップを実装するのであれば、 ondrag, ondragend, ondragleave は省略しても問題ない。

<html>
<head>
    <title>Drag and Drop Test</title>
    <script type="text/javascript" src="dragdrop.js"></script>
</head>
<body>
    <div id="DragSource"
         draggable="true"
         ondragstart="handleDragEvents(event);"
         ondrag="handleDragEvents(event);"
         ondragend="handleDragEvents(event);"
         style="border: 1px solid black; padding: 50px; margin: 10px;">Drag Source</div>
    <div id="DropTarget"
         ondragenter="handleDropEvents(event);"
         ondragover="handleDropEvents(event);"
         ondragleave="handleDropEvents(event);"
         ondrop="handleDropEvents(event);"
         style="border: 1px solid black; padding: 50px; margin: 10px;">Drop Target</div>
    <a href="http://www.mozilla.org/">http://www.mozilla.org/</a>
    <img src="http://www.mozilla.org/images/poweredby_200.gif">
</body>
</html>

サンプルコード (XUL)

2つの xul:label 要素を配置し、一方をドラッグ元、もう一方をドロップ先とする。 HTML の場合とほぼ同じであるが、すべての XUL 要素はドラッグ可能となりうるため、「draggable=”true”」属性を指定する必要は無い。

<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<window title="Drag and Drop Test"
        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
    <script type="application/x-javascript" src="dragdrop.js" />
    <label id="DragSource"
           value="Drag Source"
           ondragstart="handleDragEvents(event);"
           ondrag="handleDragEvents(event);"
           ondragend="handleDragEvents(event);"
           style="border: 1px solid black; padding: 50px; margin: 10px;" />
    <label id="DropTarget"
           value="Drop Target"
           ondragenter="handleDropEvents(event);"
           ondragover="handleDropEvents(event);"
           ondragleave="handleDropEvents(event);"
           ondrop="handleDropEvents(event);"
           style="border: 1px solid black; padding: 50px; margin: 10px;" />
</window>

サンプルコード (JavaScript)

JavaScript のソースコードは、 HTML と XUL の両者に共通となる。また、動作させるために chrome 権限は必要ない。
今回はとりあえず以下のような雛形的なイベントハンドラの処理を作っておき、あとで処理を追加する。
なお、サンプルコードでは window.dump メソッドを使用する。

function handleDragEvents(event) {
    dump("[" + event.target.id + "] " + event.type + "
");
    switch (event.type) {
        case "dragstart": 
            break;
        case "drag": 
            break;
        case "dragend": 
            break;
    }
}

function handleDropEvents(event) {
    dump("[" + event.target.id + "] " + event.type + "
");
    switch (event.type) {
        case "dragenter": 
        case "dragover": 
            break;
        case "dragleave": 
            break;
        case "drop": 
            break;
    }
}

動作確認

現段階ではドラッグ元をドラッグ開始した際の dragstart イベントしか発生しませんが、実際にドラッグしてイベントが発生することを確認してください。

参考

Drag and Drop – MDC

関連記事

Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その1~イベントハンドラの追加)
Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その2~ドラッグ処理)
Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その3~ドロップ処理)

TOP

待望の document.elementFromPoint が実装

待望の document.elementFromPoint が Firefox 3.0a8pre にて実装された。仕様は nsIDOMNSDocument.idl に詳しく書いてあるが、おおよそ以下の通りである。

  • HTML, XUL どちらの document に対しても使用可能
  • document の左上を (0, 0) とし、位置 (x, y) にある実際に見えている要素を取得する
  • 同一の document 内に存在する要素のみ取得可能。例えばインナーフレーム内の document 内に存在する要素は取得できず、代わりに iframe 要素を返す。
  • 位置 (x, y) が document の可視領域の外側にある場合、null を返す。
  • XUL document で使用する場合、例えば textbox 要素のスクロールバーのように XBL で生成された無名要素は取得できない。この場合、 textbox 要素を返す。
  • XUL document で使用する場合、 onload イベント発生以降でなければならない。

さっそく使い心地を試すべく、 HTML の document についてのサンプルをブックマークレットとして作った。使い方は、 Firefox 3 で適当なページを開き、下記のコードを丸ごとコピーしてロケーションバーへ貼り付けて移動するだけ。マウスポインタ上に位置する要素を elementFromPoint で取得してツールチップ風に表示する。

サンプル1

javascript:(function(){
    var tooltip = document.createElement("DIV");
    tooltip.style.cssText = "position: absolute; z-index: 1000; background-color: lightgreen;";
    document.body.appendChild(tooltip);
    var scanElement = function(event) {
        var elt = document.elementFromPoint(event.clientX, event.clientY);
        tooltip.innerHTML = "(" + event.clientX + ", " + event.clientY + ") " + elt.tagName;
        tooltip.style.left = (event.clientX + window.scrollX) + "px";
        tooltip.style.top  = (event.clientY + window.scrollY + 21) + "px";
    };
    document.addEventListener("mousemove", scanElement, false);
})();

しかし、上記の例はわざわざ elementFromPoint を使わなくても event.target で代用可能なので面白みがない。そこで、より機械的に指定した座標の要素を取得する例を作った。ページ左上から斜めに移動しながら要素名を取得して表示する。

サンプル2

javascript:(function(){
    var tooltip = document.createElement("DIV");
    tooltip.style.cssText = "position: absolute; z-index: 1000; background-color: lightgreen;";
    document.body.appendChild(tooltip);
    var scanElement = function(posX, posY) {
        var elt = document.elementFromPoint(posX, posY);
        tooltip.innerHTML = "(" + posX + ", " + posY + ") " + elt.tagName;
        tooltip.style.left = (posX + 2) + "px";
        tooltip.style.top  = (posY + 2) + "px";
        setTimeout(function(){ scanElement(++posX, ++posY); }, 20);
    };
    window.scrollTo(0, 0);
    scanElement(0, 0);
})();

TOP

createElement してから appendChild するまでの間のプロパティ変更

下記の value プロパティ変更は効果がない。

var elt = document.createElement("textbox");
elt.value = "It works!";
document.documentElement.appendChild(elt);

代わりに setAttribute を使うか、

var elt = document.createElement("textbox");
elt.setAttribute("value", "It works!");
document.documentElement.appendChild(elt);

appendChild してからプロパティを変更する。

var elt = document.createElement("textbox");
document.documentElement.appendChild(elt);
elt.value = "It works!";

TOP

Firefox 3 のフルページズーム使用時はスクリーン上でのピクセル量とCSS上でのピクセル量は一致しない

Firefox 3 Alpha に搭載されたフルページズーム(テキストと画像をともに拡大/縮小する機能)を使った場合、CSS レイアウトの座標上での「1ピクセル」という単位は、必ずしも画面上での1ピクセルに一致しないことになる。例えばフルページズーム機能でページを2倍に拡大した状態だと、CSSでの1pxのborderがスクリーン上では2pxとして表示される(下図参照)。

左:Firefox 2.0.0.5 で「文字サイズ」を約2倍に拡大した表示例
右:Firefox 3.0a8pre でフルページズームを使って2倍に拡大した表示例
Full Zoom

問題が生じるケース

All-in-One Gestures のマウストレイル(マウスジェスチャの軌跡描画機能)は、マウスポインタの移動に応じてスクリーン上の座標 (event.screenX, event.screenX) が変化したピクセル量分だけの点をページ上に配置することによって線を描画するという仕様である。
フルページズームでページを2倍に拡大した状態だと、スクリーン上の座標で4px移動しても、実際のCSSの座標上では2px分という計算になるが、現時点の AiOG のマウストレイルの実装はこれを考慮していない。したがって、 AiOG を無理やり Firefox 3 上で使用すると、スクリーン上の座標で4px移動したときもCSSの座標上で4px分の点を配置するため、実際のスクリーン上では8pxとして表示されてしまう。なおかつ、線の太さが2倍になる、軌跡の位置がマウスポインタから大きくずれるといった問題点が生じる。

結論

フルページズーム機能搭載に伴い、スクリーン上でのピクセル量とCSS上でのピクセル量が一致するという前提はもはや崩れ去った。両者の整合性を取るためには、 nsIMarkupDocumentViewer::fullZoom 値を常に意識する必要がある。例えば現在フォーカスしているブラウザの fullZoom 値は、以下のようにして取得可能である。

gBrowser.mCurrentBrowser.markupDocumentViewer.fullZoom

TOP