DOM 要素 (HTML ドキュメント内) が現在表示されている (ビューポートに表示されている) かどうかを確認する効率的な方法はありますか?
(質問は Firefox に関するものです。)
DOM 要素 (HTML ドキュメント内) が現在表示されている (ビューポートに表示されている) かどうかを確認する効率的な方法はありますか?
(質問は Firefox に関するものです。)
現在、ほとんどのブラウザーがgetBoundingClientRectメソッドをサポートしており、これがベスト プラクティスとなっています。古い回答を使用すると、非常に遅く、正確ではなく、いくつかのバグがあります。
正解として選択された解が正確であることはほとんどありません。
このソリューションは、Internet Explorer 7 (およびそれ以降)、iOS 5 (およびそれ以降) Safari、Android 2.0 (Eclair) 以降、BlackBerry、Opera Mobile、および Internet Explorer Mobile 9でテストされました。
function isElementInViewport (el) {
// Special bonus for those using jQuery
if (typeof jQuery === "function" && el instanceof jQuery) {
el = el[0];
}
var rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /* or $(window).height() */
rect.right <= (window.innerWidth || document.documentElement.clientWidth) /* or $(window).width() */
);
}
上記の関数が呼び出された瞬間に正しい答えを返すことは確かですが、要素の可視性をイベントとして追跡することはどうでしょうか?
<body>
タグの末尾に次のコードを配置します。
function onVisibilityChange(el, callback) {
var old_visible;
return function () {
var visible = isElementInViewport(el);
if (visible != old_visible) {
old_visible = visible;
if (typeof callback == 'function') {
callback();
}
}
}
}
var handler = onVisibilityChange(el, function() {
/* Your code go here */
});
// jQuery
$(window).on('DOMContentLoaded load resize scroll', handler);
/* // Non-jQuery
if (window.addEventListener) {
addEventListener('DOMContentLoaded', handler, false);
addEventListener('load', handler, false);
addEventListener('scroll', handler, false);
addEventListener('resize', handler, false);
} else if (window.attachEvent) {
attachEvent('onDOMContentLoaded', handler); // Internet Explorer 9+ :(
attachEvent('onload', handler);
attachEvent('onscroll', handler);
attachEvent('onresize', handler);
}
*/
もちろん、DOM の変更を行うと、要素の可視性が変更される可能性があります。
ガイドラインとよくある落とし穴:
ページのズームやモバイル デバイスのピンチを追跡する必要があるかもしれません。jQuery はズーム/ピンチクロス ブラウザーを処理する必要があります。それ以外の場合は、最初または2 番目のリンクが役立ちます。
DOMを変更すると、要素の可視性に影響を与える可能性があります。handler()
それを制御して、手動で呼び出す必要があります。onrepaint
残念ながら、クロス ブラウザーイベントはありません。一方、要素の可視性を変更する可能性のある DOM の変更に対してのみ、最適化を行い、再チェックを実行できます。
現時点でCSS が適用されているという保証はないため、決してjQuery $(document).read()内でのみ使用してください。コードはハード ドライブ上の CSS でローカルに動作しますが、リモート サーバーに配置すると失敗します。
DOMContentLoaded
が起動された後、スタイルが適用されますが、画像はまだ読み込まれていません。window.onload
したがって、イベントリスナーを追加する必要があります。
ズーム/ピンチ イベントはまだキャッチできません。
最後の手段は次のコードです。
/* TODO: this looks like a very bad code */
setInterval(handler, 600);
Web ページのタブがアクティブで表示されているかどうかが気になる場合は、HTML5 APIの素晴らしい機能pageVisibiliyを使用できます。
TODO: このメソッドは、次の 2 つの状況を処理しません。
z-index
。overflow-scroll
要素のコンテナーで使用します。更新:時間は進み、ブラウザも同様です。この手法は推奨されなくなりました。7 より前のバージョンの Internet Explorer をサポートする必要がない場合は、Dan のソリューションを使用する必要があります。
元のソリューション(現在は古くなっています):
これは、要素が現在のビューポートに完全に表示されているかどうかを確認します。
function elementInViewport(el) {
var top = el.offsetTop;
var left = el.offsetLeft;
var width = el.offsetWidth;
var height = el.offsetHeight;
while(el.offsetParent) {
el = el.offsetParent;
top += el.offsetTop;
left += el.offsetLeft;
}
return (
top >= window.pageYOffset &&
left >= window.pageXOffset &&
(top + height) <= (window.pageYOffset + window.innerHeight) &&
(left + width) <= (window.pageXOffset + window.innerWidth)
);
}
これを単純に変更して、要素の一部がビューポートに表示されているかどうかを判断できます。
function elementInViewport2(el) {
var top = el.offsetTop;
var left = el.offsetLeft;
var width = el.offsetWidth;
var height = el.offsetHeight;
while(el.offsetParent) {
el = el.offsetParent;
top += el.offsetTop;
left += el.offsetLeft;
}
return (
top < (window.pageYOffset + window.innerHeight) &&
left < (window.pageXOffset + window.innerWidth) &&
(top + height) > window.pageYOffset &&
(left + width) > window.pageXOffset
);
}
最近のブラウザでは、次の利点を提供するIntersectionObserverAPIを確認することをお勧めします。
Intersection Observerは本格的な標準になりつつあり、Chrome 51以降、Edge 15以降、Firefox 55以降ですでにサポートされており、Safari向けに開発中です。利用可能なポリフィルもあります。
ダンが提供した回答にはいくつかの問題があり、状況によっては不適切なアプローチになる可能性があります。これらの問題のいくつかは、下部にある彼の回答で指摘されており、彼のコードは次の要素に対して誤検知を与えます。
clip
プロパティを使用して非表示にされた要素またはその子これらの制限は、次の簡単なテストの結果で示されています。
isElementVisible()
これらの問題の解決策を以下に示します。テスト結果とコードの一部の説明を示します。
function isElementVisible(el) {
var rect = el.getBoundingClientRect(),
vWidth = window.innerWidth || document.documentElement.clientWidth,
vHeight = window.innerHeight || document.documentElement.clientHeight,
efp = function (x, y) { return document.elementFromPoint(x, y) };
// Return false if it's not in the viewport
if (rect.right < 0 || rect.bottom < 0
|| rect.left > vWidth || rect.top > vHeight)
return false;
// Return true if any of its four corners are visible
return (
el.contains(efp(rect.left, rect.top))
|| el.contains(efp(rect.right, rect.top))
|| el.contains(efp(rect.right, rect.bottom))
|| el.contains(efp(rect.left, rect.bottom))
);
}
テストに合格:http: //jsfiddle.net/AndyE/cAY8c/
そして結果:
ただし、この方法には独自の制限があります。たとえば、同じ場所にある別の要素よりも低いz-indexでテストされている要素は、前の要素が実際にその一部を非表示にしていない場合でも、非表示として識別されます。それでも、この方法には、Danのソリューションではカバーできない場合があります。
element.getBoundingClientRect()
とは両方ともdocument.elementFromPoint()
CSSOMワーキングドラフト仕様の一部であり、少なくともIE 6以降およびほとんどのデスクトップブラウザーで長期間サポートされています(完全ではありませんが)。詳細については、これらの関数のQuirksmodeを参照してください。
contains()
によって返される要素document.elementFromPoint()
が、可視性をテストしている要素の子ノードであるかどうかを確認するために使用されます。返される要素が同じ要素である場合もtrueを返します。これにより、チェックがより堅牢になります。これはすべての主要なブラウザでサポートされており、Firefox9.0が最後に追加されています。古いFirefoxのサポートについては、この回答の履歴を確認してください。
要素の周囲のより多くのポイントをテストして可視性を確認する場合、つまり、要素が50%を超えてカバーされていないことを確認する場合は、回答の最後の部分を調整するのにそれほど時間はかかりません。ただし、すべてのピクセルをチェックして100%表示されていることを確認すると、おそらく非常に遅くなることに注意してください。
私はダンの答えを試しましたが、境界を決定するために使用される代数は、要素がビューポートのサイズ以下であり、完全にビューポートの内側にある必要があることを意味しtrue
、簡単に偽陰性につながります。要素がビューポートにあるかどうかを判断したい場合は、ryanve の答えは近いですが、テスト対象の要素がビューポートに重なる必要があるため、これを試してください。
function isElementInViewport(el) {
var rect = el.getBoundingClientRect();
return rect.bottom > 0 &&
rect.right > 0 &&
rect.left < (window.innerWidth || document.documentElement.clientWidth) /* or $(window).width() */ &&
rect.top < (window.innerHeight || document.documentElement.clientHeight) /* or $(window).height() */;
}
公共サービスとして:
正しい計算(要素は> window、特に携帯電話の画面で可能)、正しいjQueryテスト、およびisElementPartiallyInViewportの追加によるDanの回答:
ちなみに、 window.innerWidth と document.documentElement.clientWidthの違いは、clientWidth/clientHeight にはスクロールバーが含まれていないのに対し、window.innerWidth/Height には含まれていることです。
function isElementPartiallyInViewport(el)
{
// Special bonus for those using jQuery
if (typeof jQuery !== 'undefined' && el instanceof jQuery)
el = el[0];
var rect = el.getBoundingClientRect();
// DOMRect { x: 8, y: 8, width: 100, height: 100, top: 8, right: 108, bottom: 108, left: 8 }
var windowHeight = (window.innerHeight || document.documentElement.clientHeight);
var windowWidth = (window.innerWidth || document.documentElement.clientWidth);
// http://stackoverflow.com/questions/325933/determine-whether-two-date-ranges-overlap
var vertInView = (rect.top <= windowHeight) && ((rect.top + rect.height) >= 0);
var horInView = (rect.left <= windowWidth) && ((rect.left + rect.width) >= 0);
return (vertInView && horInView);
}
// http://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport
function isElementInViewport (el)
{
// Special bonus for those using jQuery
if (typeof jQuery !== 'undefined' && el instanceof jQuery)
el = el[0];
var rect = el.getBoundingClientRect();
var windowHeight = (window.innerHeight || document.documentElement.clientHeight);
var windowWidth = (window.innerWidth || document.documentElement.clientWidth);
return (
(rect.left >= 0)
&& (rect.top >= 0)
&& ((rect.left + rect.width) <= windowWidth)
&& ((rect.top + rect.height) <= windowHeight)
);
}
function fnIsVis(ele)
{
var inVpFull = isElementInViewport(ele);
var inVpPartial = isElementPartiallyInViewport(ele);
console.clear();
console.log("Fully in viewport: " + inVpFull);
console.log("Partially in viewport: " + inVpPartial);
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<title>Test</title>
<!--
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<script src="scrollMonitor.js"></script>
-->
<script type="text/javascript">
function isElementPartiallyInViewport(el)
{
// Special bonus for those using jQuery
if (typeof jQuery !== 'undefined' && el instanceof jQuery)
el = el[0];
var rect = el.getBoundingClientRect();
// DOMRect { x: 8, y: 8, width: 100, height: 100, top: 8, right: 108, bottom: 108, left: 8 }
var windowHeight = (window.innerHeight || document.documentElement.clientHeight);
var windowWidth = (window.innerWidth || document.documentElement.clientWidth);
// http://stackoverflow.com/questions/325933/determine-whether-two-date-ranges-overlap
var vertInView = (rect.top <= windowHeight) && ((rect.top + rect.height) >= 0);
var horInView = (rect.left <= windowWidth) && ((rect.left + rect.width) >= 0);
return (vertInView && horInView);
}
// http://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport
function isElementInViewport (el)
{
// Special bonus for those using jQuery
if (typeof jQuery !== 'undefined' && el instanceof jQuery)
el = el[0];
var rect = el.getBoundingClientRect();
var windowHeight = (window.innerHeight || document.documentElement.clientHeight);
var windowWidth = (window.innerWidth || document.documentElement.clientWidth);
return (
(rect.left >= 0)
&& (rect.top >= 0)
&& ((rect.left + rect.width) <= windowWidth)
&& ((rect.top + rect.height) <= windowHeight)
);
}
function fnIsVis(ele)
{
var inVpFull = isElementInViewport(ele);
var inVpPartial = isElementPartiallyInViewport(ele);
console.clear();
console.log("Fully in viewport: " + inVpFull);
console.log("Partially in viewport: " + inVpPartial);
}
// var scrollLeft = (window.pageXOffset !== undefined) ? window.pageXOffset : (document.documentElement || document.body.parentNode || document.body).scrollLeft,
// var scrollTop = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop;
</script>
</head>
<body>
<div style="display: block; width: 2000px; height: 10000px; background-color: green;">
<br /><br /><br /><br /><br /><br />
<br /><br /><br /><br /><br /><br />
<br /><br /><br /><br /><br /><br />
<input type="button" onclick="fnIsVis(document.getElementById('myele'));" value="det" />
<br /><br /><br /><br /><br /><br />
<br /><br /><br /><br /><br /><br />
<br /><br /><br /><br /><br /><br />
<div style="background-color: crimson; display: inline-block; width: 800px; height: 500px;" ></div>
<div id="myele" onclick="fnIsVis(this);" style="display: inline-block; width: 100px; height: 100px; background-color: hotpink;">
t
</div>
<br /><br /><br /><br /><br /><br />
<br /><br /><br /><br /><br /><br />
<br /><br /><br /><br /><br /><br />
<input type="button" onclick="fnIsVis(document.getElementById('myele'));" value="det" />
</div>
<!--
<script type="text/javascript">
var element = document.getElementById("myele");
var watcher = scrollMonitor.create(element);
watcher.lock();
watcher.stateChange(function() {
console.log("state changed");
// $(element).toggleClass('fixed', this.isAboveViewport)
});
</script>
-->
</body>
</html>
getBoundingClientRectを使用するvergeのソースを参照してください。それは次のようなものです:
function inViewport (element) {
if (!element) return false;
if (1 !== element.nodeType) return false;
var html = document.documentElement;
var rect = element.getBoundingClientRect();
return !!rect &&
rect.bottom >= 0 &&
rect.right >= 0 &&
rect.left <= html.clientWidth &&
rect.top <= html.clientHeight;
}
要素の一部がビューポートにあるtrue
場合に返されます。
これはより機能的な方法だと思います。 ダンの答えは、再帰的なコンテキストでは機能しません。
この関数は、HTML タグまでの任意のレベルを再帰的にテストすることで要素が他のスクロール可能な div 内にある場合の問題を解決し、最初の false で停止します。
/**
* fullVisible=true only returns true if the all object rect is visible
*/
function isReallyVisible(el, fullVisible) {
if ( el.tagName == "HTML" )
return true;
var parentRect=el.parentNode.getBoundingClientRect();
var rect = arguments[2] || el.getBoundingClientRect();
return (
( fullVisible ? rect.top >= parentRect.top : rect.bottom > parentRect.top ) &&
( fullVisible ? rect.left >= parentRect.left : rect.right > parentRect.left ) &&
( fullVisible ? rect.bottom <= parentRect.bottom : rect.top < parentRect.bottom ) &&
( fullVisible ? rect.right <= parentRect.right : rect.left < parentRect.right ) &&
isReallyVisible(el.parentNode, fullVisible, rect)
);
};
dan's solutionに基づいて、実装をクリーンアップして、同じページで複数回使用するのが簡単になるようにしました。
$(function() {
$(window).on('load resize scroll', function() {
addClassToElementInViewport($('.bug-icon'), 'animate-bug-icon');
addClassToElementInViewport($('.another-thing'), 'animate-thing');
// repeat as needed ...
});
function addClassToElementInViewport(element, newClass) {
if (inViewport(element)) {
element.addClass(newClass);
}
}
function inViewport(element) {
if (typeof jQuery === "function" && element instanceof jQuery) {
element = element[0];
}
var elementBounds = element.getBoundingClientRect();
return (
elementBounds.top >= 0 &&
elementBounds.left >= 0 &&
elementBounds.bottom <= $(window).height() &&
elementBounds.right <= $(window).width()
);
}
});
私が使用している方法は、要素がスクロールして表示されるときに、CSS キーフレーム アニメーションをトリガーするクラスを追加することです。これは非常に簡単で、ページ上で条件付きでアニメーション化するものが 10 以上ある場合に特に効果的です。
より良い解決策:
function getViewportSize(w) {
var w = w || window;
if(w.innerWidth != null)
return {w:w.innerWidth, h:w.innerHeight};
var d = w.document;
if (document.compatMode == "CSS1Compat") {
return {
w: d.documentElement.clientWidth,
h: d.documentElement.clientHeight
};
}
return { w: d.body.clientWidth, h: d.body.clientWidth };
}
function isViewportVisible(e) {
var box = e.getBoundingClientRect();
var height = box.height || (box.bottom - box.top);
var width = box.width || (box.right - box.left);
var viewport = getViewportSize();
if(!height || !width)
return false;
if(box.top > viewport.h || box.bottom < 0)
return false;
if(box.right < 0 || box.left > viewport.w)
return false;
return true;
}
これは、要素が少なくとも部分的に表示されているかどうかを確認します (垂直寸法)。
function inView(element) {
var box = element.getBoundingClientRect();
return inViewBox(box);
}
function inViewBox(box) {
return ((box.bottom < 0) || (box.top > getWindowSize().h)) ? false : true;
}
function getWindowSize() {
return { w: document.body.offsetWidth || document.documentElement.offsetWidth || window.innerWidth, h: document.body.offsetHeight || document.documentElement.offsetHeight || window.innerHeight}
}
ここでのすべての答えは、要素が何らかの方法で表示されるだけでなく、ビューポート内に完全に含まれているかどうかを判断することです。たとえば、ビューの下部に画像の半分しか表示されていない場合、ここでのソリューションは「外側」であることを考慮して失敗します。
を介して遅延読み込みを行うユースケースがありましたIntersectionObserver
が、ポップイン中にアニメーションが発生するため、ページの読み込み時に既に交差している画像を観察したくありませんでした。そのために、次のコードを使用しました。
const bounding = el.getBoundingClientRect();
const isVisible = (0 < bounding.top && bounding.top < (window.innerHeight || document.documentElement.clientHeight)) ||
(0 < bounding.bottom && bounding.bottom < (window.innerHeight || document.documentElement.clientHeight));
これは基本的に、上または下の境界がビューポート内に独立しているかどうかを確認することです。反対側の端は外側にある場合がありますが、一方の端が内側にある限り、少なくとも部分的に「見えます」。
同様の課題については、 scrollIntoViewIfNeeded()のポリフィルを公開するこの要点を本当に楽しみました。
答えるために必要なすべてのカンフーは、このブロック内にあります。
var parent = this.parentNode,
parentComputedStyle = window.getComputedStyle(parent, null),
parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')),
parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')),
overTop = this.offsetTop - parent.offsetTop < parent.scrollTop,
overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight),
overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft,
overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth),
alignWithTop = overTop && !overBottom;
this
overTop
たとえば、または-ドリフトを取得する必要があるかどうかを知りたい要素を指しますoverBottom
...