Hatena::Diary

by edvakf in hatena

2010-02-05

要素が画面上に見えているかどうかを調べる

document.elementFromPoint という便利な関数を知ったので、今作っている ChromeMigemo ページ内検索で使ってみた。

これが困ったことに、ブラウザごとにかなり挙動が違うのだけど、本来の動作はこんな感じらしい。

待望の 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 イベント発生以降でなければならない。
SCRAPBLOG : 待望の document.elementFromPoint が実装

何かの仕様に書いてあるのだろうか? と思ってググってみたら、CSSOM にあった。

The elementFromPoint(x, y) method, when invoked, must return the element at coordinates x,y in the viewport. The element to be returned is determined through hit testing. If either argument is negative, x is greater than the viewport width excluding the size of a rendered scroll bar (if any), or y is greather than the viewport height excluding the size of a rendered scroll bar (if any), the method must return null. If there is no element at the given position the method must return the root element, if any, or null otherwise.

CSSOM View Module

これがブラウザ間でどう違うかっていうと…

IEやFirefoxの場合はウィンドウの表示領域の左上が基準だが、OperaやSafaiはページ全体の左上を基準とした座標で指定しなくてはならない。

そしてGoogle Chromeだがバージョン3ではOperaSafariと同じくページ全体の左上が基準なのだが、バージョン4からはIEやFirefoxと同じ表示しているウィンドウの左上が基準となったようです。

kiyohoge Google Chromeのバージョン3と4ではelementfrompointの座標の基準が違う

本来なら要素 (の左上部分) が見えているかどうかはこれだけで判定できるみたい。

function is_in_view(elem) {
  var rect = document.getBoundingClientRect();
  return elem === document.elementFromPoint(rect.left, rect.top);
}

Chromium (5系), Safari, Firefox, Opera 10.10, Opera 10.50 で試してみた。うまくいったのは Chromium だけだった。


Firefox は、どうやら (getBoundingClientRect か elementFromPoint のどちらかが) 1px ずれて計算されているみたいなので、このようにしないといけない。

function is_in_view(elem) {
  var rect = document.getBoundingClientRect();
  return elem === document.elementFromPoint(rect.left + 1, rect.top + 1);
}

input 要素とかだと +1 しなくても取得できるんだけど、div とか p とかテキスト系の要素は取得できない。


Safari の場合はスクロール量を足してあげないとだめ (Chrome と挙動が違うのは WebKit のバージョンのせい。https://bugs.webkit.org/show_bug.cgi?id=29219 で直ってる)。スクロール量は Safari では document.body.scrollLeft/Top で取れるので、

function is_in_view(elem) {
  var rect = document.getBoundingClientRect();
  return elem === document.elementFromPoint(rect.left + document.body.scrollLeft, rect.top + document.body.scrollTop);
}

となる。


Opera 10.10 では、互換モードだとスクロール量が document.body.scrollLeft/Top で取れるんだけど、標準モードだと document.documentElement.scrollLeft/Top にしないといけない。たしか、単に数字の大きい方を使えばよかったと思う。

それから、Opera は elementFromPoint でウィンドウ内に見えていない部分の要素まで取得できてしまうので、(これはこれで嬉しいのだけど) それらは除くようにする。

function is_in_view(elem) {
  var scrollLeft = Math.max(d.documentElement.scrollLeft, d.body.scrollLeft);
  var scrollTop = Math.max(d.documentElement.scrollTop, d.body.scrollTop);

  if (rect.left < 0 || rect.left > window.innerWidth || rect.top < 0 || rect.top > window.innerHeight) return false;

  var rect = document.getBoundingClientRect();
  return elem === document.elementFromPoint(rect.left + scrollLeft, rect.top + scrollTop);
}

最後に Opera 10.50 では elementFromPoint がドキュメントの座標ではなくてウィンドウの座標を基準にするようになっている。ただしウィンドウ外の要素も返すのはそのまま。

function is_in_view(elem) {
  if (rect.left < 0 || rect.left > window.innerWidth || rect.top < 0 || rect.top > window.innerHeight) return false;

  var rect = document.getBoundingClientRect();
  return elem === document.elementFromPoint(rect.left, rect.top);
}

以上を踏まえてブックマークレットを作ってみた。

要素の左上部分が見えてるものには、赤い ■ を付けてくれる。


javascript:(function(d) {

var scrollLeft = Math.max(d.documentElement.scrollLeft, d.body.scrollLeft);
var scrollTop = Math.max(d.documentElement.scrollTop, d.body.scrollTop);
var ua = {str:navigator.userAgent};
if (window.opera) {
  if (window.opera.version() >= 10.50) ua.opera1050 = 1;
  else ua.opera = 1;
}else if (/Chrome/.test(ua.str)) ua.chrome = 1;
else if (/AppleWebKit/.test(ua.str)) ua.safari = 1;
else if (/Gecko\//.test(ua.str)) ua.firefox = 1;

[].forEach.call(d.getElementsByTagName('*'), function(elem) {

  var rect = elem.getClientRects()[0];
  if (!rect) return;
  if ((ua.opera || ua.opera1050) && (rect.left < 0 || rect.left > window.innerWidth || rect.top < 0 || rect.top > window.innerHeight)) return false;

  var left = rect.left;
  var top = rect.top;

  if (ua.opera || ua.safari) {
    left += scrollLeft;
    top += scrollTop;
  }
  if (ua.firefox) {
    left += 1;
    top += 1;
  }

  if (d.elementFromPoint(left, top) === elem) {

    var div = d.createElement('div');
    div.setAttribute('style', 'position:absolute;top:'+(rect.top+scrollTop)+'px;left:'+(rect.left+scrollLeft)+'px;height:'+10+'px;width:'+10+'px;background-color:red;opacity:0.5;');
    d.body.appendChild(div);
  }
});

})(document);

これを使えば Hit-a-Hint がものすごくラクチンにできる!!

実際に使う場合は WebKit のバージョンで判定したほうがいいな。Safari と iPhone と Chrome と Android のブラウザと、全部試すのは無理だし。あと OperaOpera 10.10 (Presto 2.2) と 10.50 (Prest 2.5) の間にモバイルだけの Presto 2.4 というのがあったはず。どこで分けたらいいのか…

IE では getBoundingClientRect の計算が 2px ずれるとかどうとか。持ってないのでわかりません。試した人は教えてください。

2010-02-02

ある要素が見える位置までスクロール

into_viewport という関数を作ってみた。

Chrome 以外ではチェックしてない。(でも IE 含め、他のブラウザで使えない関数などは使ってないはず)

IE に getComputedStyle が無いのを忘れてた。var s = getComputedStyle(elem, null); のところを var s = window.getComputedStyle ? getComputedStyle(elem, null) : elem.currentStyle; にしたらいいのかな? (IE 持ってないのでわからない)

微修正。body や html 要素に overflow:auto などが着いてると変なことになる件。あと、FirefoxOpera では互換モードのとき document.body.scrollTop が使えて、標準モードのときは document.documentElement.scrollTop を使わないといけない。Chrome と Safari は標準、互換とも document.body.scrollTop で OK。UserAgent で判定するのは不本意だけど、このへんは好きな判定方法を使ったらいい。

function into_viewport(elem) {
  var target = elem;

  while (elem = elem.parentNode) {
    if (elem === document.body || elem === document.documentElement) break;
    var s = window.getComputedStyle ? getComputedStyle(elem, null) : elem.currentStyle;
    if (s && /auto|scroll/.test(s.overflowX + s.overflowY)) {alert([target,elem]);
      scroll_to_element(target, elem);
      into_viewport(elem);
      return;
    }
  }

  var origin = (document.compatMode !== 'BackCompat' && (window.opera || navigator.userAgent.indexOf('Gecko/'))) ? document.documentElement : document.body;
  scroll_to_element(target, origin);
}

function scroll_to_element(elem, origin) {
  if (origin === document.body || origin === document.documentElement) {
    var outer = {left: 0, right: window.innerWidth, top: 0, bottom: window.innerHeight};
  } else {
    var outer = origin.getBoundingClientRect();
  }
  var inner = elem.getBoundingClientRect();
  var x = origin.scrollLeft;
  var y = origin.scrollTop;

  var flag = 0;
  if ((outer.left > inner.left || outer.right < inner.right) && ++flag)
    x += (inner.left + inner.right) / 2 - (outer.left + outer.right) / 2;
  if ((outer.top > inner.top || outer.bottom < inner.bottom) && ++flag)
    y += (inner.top + inner.bottom) / 2 - (outer.top + outer.bottom) / 2;

  //if (flag) {origin.scrollLeft = x; origin.scrollTop = y;}  // スムーズスクロールしない場合

  if (flag) new Tween(origin, { // スムーズスクロールする場合
    time: 0.1,
    scrollLeft: {
      to: x
    },
    scrollTop: {
      to: y
    }
  });
}

Prototype.js の Element.scrollTo だと overflow: scroll/auto な親要素をわざわざスクロールしてくれないと思う。(ちらっとコード見ただけなので違ってるかも)

Tweenid:os0x さんの AutoPatchwork から持ってきたのを微修正して使っている。


ところで、

上のコードは、実は Chrome 用の Migemo ページ内検索を作ってるときに書いたのだけど、もし試したい人がいれば試してください。

正式に公開しました→Migemoでページ内検索するためのGoogle Chrome Extension - by edvakf in hatena




ソースを一つのフォルダに入れ、load unpacked extension でそのフォルダを指定。ChromeMigemo Extension が必須。

今のところキーバインドは、/ キーで検索開始、↓↑で次と前を選択、; でフォーカス。

ほとんど完成しているんだけど、一つだけ気になるのが、普通だったら↓のように "1 or 5" というのがこの位置に出るところが、

f:id:edvakf:20100202155752p:image

何故かこのページだけ右に大きくずれて出てしまう (↓うっすら見える?)。理由が分からなくてお手上げです。この件は修正した。

f:id:edvakf:20100202155919p:image

その他バグを発見したら教えてください。


MigemoFindInPage は最終的に ChromeKeyConfig と連携させるかマージする方向でいこうと思っている。キーハンドリングは出来ればやりたくない。

2010-01-30

ChromeでGIFアニメーションを止める

Chrome だって Opera みたいにアニメーション画像を止めたい!


ってなことで、XMLHttpRequest でバイナリを読み込む方法GIF アニメーションの1フレームだけを取り出して、data:URL にする方法でやってみた。

f:id:edvakf:20100130144827p:image

止めた画像の上では、カーソルこういうアイコンになって、クリックするとまた動きだす。


インストール


↓ソース


ちょっと大きな画像になると

replace(/[\u0100-\uffff]/g, function(c){
  return String.fromCharCode(c.charCodeAt(0) & 0xff);
})

の部分が1秒近くかかってしまうことがあるのが悩みどころ…何か良い方法はないだろうか。

それから、DOMNodeInserted とか DOMAttrChanged を監視してるので、Chrome を遅くする可能性もある。承知の上で使ってね。


GIF の仕様はこちらを参考にした。

GIF Header のところで "Global Color Table(0〜255×3B)" と書いてあるのは、"Global Color Table(0〜256×3B)" の誤りですね。


もし気に入ってくれたら、Extension のページで星を付けてくれると僕が喜びます (ボソっ)