Hatena::ブログ(Diary)

latest log このページをアンテナに追加 RSSフィード

2010-07-09

CSSセレクタを書き直したなど

2年ぶりにCSSセレクタを書き直しました

前回との違いは

  • 前作の精度を98点とすると今回は96点ぐらい。W3Cの意地悪テストを幾つかスキップ
  • コード量が半分に。実行速度はほぼ同じ
  • jQuery拡張, :active, :visited は非サポート(:visited は空の配列を返す)
  • W3Cの仕様に無い :contains(), E[ATTR != VALUE] は非サポート
    • MobileWebKitモードでは動作せず。フルビルドモードでは一応動作させてる
  • uu.query(expr) が返すNodeArrayはドキュメントオーダーでソート済みの状態に
  • 前回は2ヶ月かけて開発。今回は3日

メジャーライブラリとの速度比較

http://pigs.sourceforge.jp/blog/CSSSelector/CSSSelectorTestSuite/slickspeed/

query2(API OFF) が js製のCSSセレクタスコアで、query2(API ON) が querySelectorAll APIを利用した場合のスコアです。遅いブラウザ/古いブラウザで試すと分かりやすいかも。

ライブラリが読み込まれていないためにテストが途中で止まる事があります、そのような場合は(コントロールキーを押しながら)何度かリロードしてみてください(SlickSpeed のバグです)。SlickSpeed は iPhone だと動かないようです。

2010-05-01

IE8とIE9pp の非互換性(event)

IE9正式版ではなく IE9pp(IE9 Platform Preview) や IE9pp2 の現状について記載しています。

IE9ppがDOMの仕様に合わせたため、IE8までとは互換性がなくなっている部分をちょこちょこ書いていきます。

IE9pp の event

イベントハンドラが受け取る event オブジェクトDOM Level3 Event の仕様に従うようになりました。

IE9pp では event.button の値が変化

event.button の値が 0(left click), 1(mid click), 2(right click) になりました。

IE8 までは各ボタンの同時押しを判定できたため、どのボタンが押されているかはビットアンドで比較する必要がありました。

  • event.button & 0x1 が true なら left click
  • event.button & 0x4 が true なら mid click
  • event.button & 0x2 が true なら right click
これからはこんな感じになります

function handleEvent(event) {
  if (IE6 || IE7 || IE8) {

      event = event || window.event;

      var msg = [];

      if (event.button & 1) {
          msg.push("left");
      }
      if (event.button & 2) {
          msg.push("right");
      }
      if (event.button & 4) {
          msg.push("mid");
      }
      msg.length && alert(msg.join(" ") + " button");
  } else {
      // IE9pp, Gecko, WebKit, Opera
      switch (event.button) {
      case 0: alert("left button"); break;
      case 1: alert("mid button"); break;
      case 2: alert("right button");
      }
  }
}

node.addEventListener("mousedown", handleEvent, false);

event.button は DOM 仕様に従うようになりましたが、window.event.button の値は IE8 と同じものが取れてきます。

あまり良い案ではありませんが、IE8 までのコードを生かしたい場合は、window.event.button を見るようにコードを修正する逃げ方もできそうです。

event.offsetX/Y が無い

IE9pp2 でも event.offsetX/Y が実装されていません。

他のブラウザと互換性がなく、IE8とも互換性がありません。

cloneNode でイベントもクローンされてしまう

IE8 までは cloneNode でイベントもクローンされていましたが、IE9pp2 でも同様です。

他のブラウザと互換性がありません。

IE9pp2 は DOMContentLoaded をサポート済み

DOMContentLoaded はIE9pp2で既に動きます。

http://blogs.msdn.com/ie/archive/2010/03/26/dom-level-3-events-support-in-ie9.aspx

<!doctype html><script>document.addEventListener("DOMContentLoaded",function(){alert(9)},false)</script>

また後で追記します。

2010-04-30

event.offsetX と offsetY の互換性について

IE9正式版ではなく IE9pp(IE9 Platform Preview) の現状について記載しています。

event.offsetX/Y や event.layerX/Y から相対座標を取得できますが、DOM Lv0 のため仕様が無く、互換性もありません。今日はそれらの非互換性について調べてみました。

event.offsetX/Y は、IE, Opera, WebKit で定義されており、event.layerX/Y は、GeckoWebKit で定義されています(WebKitのlayerX/YはGeckoと別物です)。

event.offsetX/Y は、現在標準化にむけて策定中です http://www.w3.org/TR/cssom-view/#extensions-to-the-mouseevent-interface

策定中の仕様では event.offsetX/Y は padding edge を基準にするため、IE6IE8 の仕様に合わせることになりそうです。

IE の動作に合わせると、遅いブラウザ(IE)では何もしなくてもよくなり、速いブラウザで計算量が増えることになりますが、(今回は)ライブラリを実装する側としてはむしろ都合が良かったりします。

デモ

mousemove イベントを使い、各ブラウザの返す値を調べてみます

http://pigs.sourceforge.jp/blog/20100430/test/demo.event/uu.event.htm

f:id:uupaa:20100430021054p:image

# 左から、IE8, Opera10.52, Safari4, Firefox3.6.3

赤いカーソルの先端が、offsetX = 0, offsetY = 0 になります。

<style>
.relative {
    position: relative; left: 100px; top: 200px;
    padding: 10px;
    border: 10px solid navy;
    background-color: blue;
}
</style>
[CSS2.1 box model] http://www.w3.org/TR/CSS2/box.html

    B-------border--------+ -> border edge [CSS2.1 keyword]
    |                     |
    |  P----padding----+  | -> padding edge [CSS2.1 keyword]
    |  |               |  |
    |  |  C-content-+  |  | -> content edge [CSS2.1 keyword]
    |  |  |         |  |  |
    |  |  |         |  |  |
    |  |  +---------+  |  |
    |  +---------------+  |
    |                     |
    +---------------------+

    B = event.offsetX/Y in WebKit
        event.layerX/Y  in Gecko
    P = event.offsetX/Y in IE6 ~ IE8
    C = event.offsetX/Y in Opera

デモを動かすと、

  • Gecko の event.layerX/Y は、B の位置をゼロとした値。layerX/Y は正の値だけ。
  • WebKit の event.offsetX/Y は、B の位置をゼロとした値。offsetX/Y は正の値だけ。
  • IE の event.offsetX/Y は、P の位置をゼロとした値。offsetX/Y には負の値がありうる。
  • Opera の event.offsetX/Y は、C の位置をゼロとした値。offsetX/Y には負の値がありうる。

といったことが分かります。

絶対座標の取得

スクロール座標を加味したページ左上からの絶対座標は、event.pageX/Y または event.clientX/Y + スクロール成分で取得できます。

IE6IE8 には event.pageX/Y が無いため、以下のようにします。

  event.pageX = event.clientX + document.documentElement.scrollLeft;
  event.pageY = event.clientY + document.documentElement.scrollTop;

相対座標の取得

イベントターゲットの要素を基準とした相対座標(最寄の要素から見た相対座標)は、event.offsetX/Y から取ります。

ボーダーやパディングが設定されていなければ取れてくる値には互換性があります。

ボーダーやパディングが設定されている場合は…

  • operaなら、getComputedStyle で border-top-width, border-left-width と padding-top, padding-left を取得し、それらを event.offsetX/Y の値に足す
  • IE なら currentStyle から border-top-width, border-left-width の値を取得し、単位を pixel に変換し、event.offsetX/Y の値に足す

といった面倒な手順が必要になります。

IEで、border-width に px 以外の単位を指定すると、リフローを伴う単位変換処理が必要になり遅くなります。

イベントハンドラ内部で毎回変換するのはやめたほうが良いでしょう。

IE9pp には offsetX/Y が無い

IE9pp には event.pageX/Y, event.offsetX/Y も存在せず、event.clientX/Y と event.screenX/Y だけが実装されています。

今のところ簡単に相対座標を取得する方法は提供されていないようです。

耳寄り

window.eventの方ならevent.x とevent.offsetXは取れますね。単に実装漏れかと。

via os0x http://twitter.com/os0x/status/13073522323

IE9pp の window.event はIE8以下との互換用で、 handleEvent(event) の event は DOM Level3 Event にほぼ従ってるっぽい。 window.event には relatedTarget などがないの

via http://twitter.com/uupaa/status/13115227590

window.event には x, y, offsetX, offsetY が残ってます。また、window.event.offsetX/Y の原点は(P = padding edge) でIE6IE8と変わりないため、不足している情報は(とりあえず今は)そちらを参照すればよさそうです

id:os0xさん ナイスな情報ありがとうございます!

2010-04-10

140文字以内で DOMContentLoaded

DOM構築完了またはそれ相当のタイミングで (どこかで定義されている)関数A をコールバックします。

function Z(){try{(new Image).doScroll();A()}catch(e){setTimeout(Z,0)}}
+[1,]?document.addEventListener("DOMContentLoaded",A,!1):Z()

130byteで驚きの読み辛さ。

ヒューマンリーダブル版

(function() {

    function IEDOMContentLoaded(){
        try {
            (new Image()).doScroll(); // [!] throws
            A();
        } catch(err) {
            setTimeout(IEDOMContentLoaded, 64); // delay after 64ms
        }
    }

    // IE6〜IE8を判定(IE8以下ならNaN, それ以外なら1)
    if (+[1,]) {

        // WebKit(525+), Safari3.1+, Google Chrome
        // Gecko, Firefox2+
        // Opera9+
        // IE9(maybe) http://bit.ly/bb0oGw
        document.addEventListener("DOMContentLoaded", A, false):

    } else {

        // IE6
        // IE7
        // IE8
        IEDOMContentLoaded();
    }
})();

130byte版では、setTimeout(Z,0); としてますが、0だと無駄に処理が走るので、DOMContentLoaded ⇒ window.onload を保障するようにガードをキッチリ入れている場合は 64ms ぐらいが妥当かなと思われます。

# 64ms は IE の setTimeout/setInterval の最小分解能が16ms なので、その倍数から。

参考リンク

2010-04-01

mousemove + elementFromPoint でマウスカーソルが示すノードを取得

mousemove と elementFromPoint を使うと、マウスカーソル直下のノードを取得できます。

デモ

http://pigs.sourceforge.jp/blog/20100401/test/uu.Class.ElementFromPoint.htm

IE6+, Safari3.1+, Firefox3+, Google Chrome4+, Opera9.5+ で動きます。

コード

// === elementFromPoint ===
//{{{!depend uu, uu.class
//}}}!depend

uu.Class.ElementFromPoint || (function(win, doc, uu) {

// client coordinates
//      true:  Safari3x, Safari4, Opera9.x, Opera10.10
//      false: IE, Gecko, Chrome4, Opera10.50
var _clientCoordinates = uu.ver.safari || (uu.ver.opera && !uu.ver.jit);

// uu.Class.ElementFromPoint
uu.Class("ElementFromPoint", {
    init:         init,       // init(callback:Function)
    handleEvent:  handleEvent // handleEvent(evt:EventObjectEx)
});

// uu.Class.ElementFromPoint.init
function init(callback) { // @param Function: callback
    this._callback = callback;

    uu.event(doc, "mousemove+", this); // document.addEventListener("mousemove", true) 相当
}

// uu.Class.ElementFromPoint.handleEvent
function handleEvent(evt) { // @param EventObjectEx:
    var node, px, py;

    // クロスブラウザなあーだこーだ
    if (uu.ie) {                     // IE
        px = evt.clientX;
        py = evt.clientY;
    } else if (_clientCoordinates) { // Safari, Opera9.x - Opera10.10
        px = evt.clientX + win.pageXOffset;
        py = evt.clientY + win.pageYOffset;
    } else {                         // IE, Firefox, Google Chrome
        px = evt.pageX - win.pageXOffset;
        py = evt.pageY - win.pageYOffset;
    }
    node = doc.elementFromPoint(px, py);

    // Opera9.x has text-node
    if (node.nodeType === 3) {
        node = node.parentNode;
    }
    if (node) {
        this._callback(evt, node);
    }
}

})(window, document, uu);
<script>
function xboot(uu) {
    // このへんはオマケ
    var tgts = uu.klass("tgt"), grips = uu.klass("grip");
    uu.factory("Drag", tgts[0], grips[0], { wheel: 1, minw: 100, minh: 100, maxw: 200 });
    uu.factory("Drag", tgts[1], grips[1], { wheel: 1 });
    uu.factory("Draggable", uu.klass("draggable"), uu.klass("droppable"));

    // このへんが本題
    uu.factory("ElementFromPoint", function(evt, node) {
        var info = uu.format("? (?,?) - ? - ?",
                             node.tagName, evt.px, evt.py,
                             uu.xpath(node), uu.json(node));

        window.status = info;
        if (!uu.ver.ie6) {
            uu.id("trace").innerText = info;
        }
    });
}
</script>

より詳しくは

@edvakf さんのすてきなエントリをどうぞ。

http://d.hatena.ne.jp/edvakf/20100205/1265338487


ちょっとしたこと

セレクタボックスをドロップダウンした状態で取れてくるノードが、Windows Safari と Mac Safari では異なったりしますし、ブラウザごとに細かい差異が色々と見つかります。

DOM Lv 0 な API が仕様化されないのは「好き勝手に実装されてて(違いが大きすぎて)標準化できない」ということなのかも。

CSSOM View Module で検討中なのを、すっかり忘れてました。http://www.w3.org/TR/cssom-view/#the-documentview-interface