diff options
author | Peter Bengtsson <mail@peterbe.com> | 2020-12-08 14:40:17 -0500 |
---|---|---|
committer | Peter Bengtsson <mail@peterbe.com> | 2020-12-08 14:40:17 -0500 |
commit | 33058f2b292b3a581333bdfb21b8f671898c5060 (patch) | |
tree | 51c3e392513ec574331b2d3f85c394445ea803c6 /files/ja/web/api/intersection_observer_api | |
parent | 8b66d724f7caf0157093fb09cfec8fbd0c6ad50a (diff) | |
download | translated-content-33058f2b292b3a581333bdfb21b8f671898c5060.tar.gz translated-content-33058f2b292b3a581333bdfb21b8f671898c5060.tar.bz2 translated-content-33058f2b292b3a581333bdfb21b8f671898c5060.zip |
initial commit
Diffstat (limited to 'files/ja/web/api/intersection_observer_api')
-rw-r--r-- | files/ja/web/api/intersection_observer_api/index.html | 597 | ||||
-rw-r--r-- | files/ja/web/api/intersection_observer_api/timing_element_visibility/index.html | 565 |
2 files changed, 1162 insertions, 0 deletions
diff --git a/files/ja/web/api/intersection_observer_api/index.html b/files/ja/web/api/intersection_observer_api/index.html new file mode 100644 index 0000000000..4b15e2bd54 --- /dev/null +++ b/files/ja/web/api/intersection_observer_api/index.html @@ -0,0 +1,597 @@ +--- +title: Intersection Observer API +slug: Web/API/Intersection_Observer_API +tags: + - API + - Clipping + - Intersection + - Intersection Observer API + - IntersectionObserver + - Overview + - Performance + - Reference + - Web + - 交差 + - 交差監視 +translation_of: Web/API/Intersection_Observer_API +--- +<div>{{DefaultAPISidebar("Intersection Observer API")}}</div> + +<p class="summary"><span class="seoSummary">Intersection Observer API (交差監視 API) は、ターゲットとなる要素が、祖先要素もしくは文書の最上位の{{Glossary("viewport", "ビューポート")}}と交差する変更を非同期的に監視する方法を提供します。</span></p> + +<p>従来、要素の可視性や、二つの要素間で互いに相対的な可視性を検出することは難しく、どの解決方法も不確実であり、ブラウザーやユーザーがアクセスするサイトの反応を鈍くする要因の一つとなっていました。ウェブが成熟していくにつれてこのような情報の必要性は高まっていきます。 Intersection (要素間交差) についての情報は下記のような理由から必要とされています。</p> + +<ul> + <li>ページがスクロールした際の画像やその他のコンテンツの遅延読み込み。</li> + <li>「無限スクロール」をするウェブサイトを実装し、スクロールに従って次々とコンテンツを読み込んで、ユーザーがページの切り替えをせずに済むようにすること。</li> + <li>広告費用を計算するための広告が表示されたかどうかのレポート。</li> + <li>ユーザーが見るかどうかによって、タスクを実行するかどうか、アニメーションを処理するかを決定すること。</li> +</ul> + +<p>これまで、要素間の交差を検出する実装をするには、 {{domxref("Element.getBoundingClientRect()")}} のようなメソッドを呼び出すイベントハンドラーやループがあり、影響を受ける要素に対する情報を都度計算し集めることで構成されていました。このようなコードがメインスレッドで実行されると、いずれかはパフォーマンスの問題を引き起こす可能性があります。試しにサイトにテストとして読み込めば分かりますが、事態は完全に酷くなりえます。</p> + +<p>ウェブページで無限スクロールを使用することを考えてみてください。ベンダーから提供されるライブラリを使用して、ページ全体に定期的に配置された広告を管理し、アニメーショングラフィックスを表示し、通知ボックスなどを描画するカスタムライブラリを使用します。これらのそれぞれには独自の Intersection を検出するためのルーチンがあり、すべてがメインスレッド上で実行されます。ウェブサイトの作者は、これが起こっていることを認識していないかもしれません。なぜなら、彼らは内部の働きについてほとんど知らずに2つのライブラリを使用しているからです。ユーザーがページをスクロールすると、スクロール処理中にこれらの Intersection の検出ルーチンが絶えず起動し、ユーザーはブラウザー、ウェブサイト、およびコンピュータにイライラさせられることになります。</p> + +<p>Intersection Observer API を使用することで、監視したい要素が別の要素 (もしくは{{Glossary("viewport")}}) に入ってきたり出ていったりする時、まだ両要素が交差する量がある一定の量を満たす時、実行されるコールバック関数を登録するが出来ます。こういった方法を用いることで、この手の要素交差を監視するためにサイトはメインスレッド上で何もする必要がなくなり、ブラウザーは要素間交差の管理を最適化して自由に行えます。</p> + +<p>Intersection Observer API を使用してできないものの1つは、重複するピクセル数または具体的なピクセル数です。ただし、「<em>N</em>%前後のどこかで交差する場合に何かしたい」という一般的な利用法はカバーされています。</p> + +<h2 id="Intersection_observer_concepts_and_usage" name="Intersection_observer_concepts_and_usage">Intersection observer 概念と使い方</h2> + +<p>Intersection Observer API を使用すると、ある要素が、これを<strong>ターゲット</strong>と呼びますが、端末のビューポートまたは指定された要素 - API の目的からこれを<strong>ルート要素</strong>もしくは<strong>ルート</strong>と呼びます - と交差するたびに呼び出されるコールバックを構成することができます。通常は、要素の直近のスクロール可能な祖先、または、要素がスクロール可能な要素の子孫でない場合はビューポートに関する交差状態の変更を監視したいでしょう。ルート要素に関する交差を監視するには、 <code>null</code> を指定してください。</p> + +<p>ビューポートとその他の要素のどちらがルートとして使用されていても、 API は同じように動作し、ターゲット要素の表示状態が変わってルートとの間で交差の量の期待値を通るたびに、提供したコールバック関数が実行されます。</p> + +<p>ターゲット要素とそのルート要素の交差する度合いが<strong>交差率</strong>です。これはターゲット要素のパーセンテージを 0.0 から 1.0 の間の値で表現したものです。</p> + +<h3 id="Creating_an_intersection_observer" name="Creating_an_intersection_observer">Intersection obserer の作成</h3> + +<p>コンストラクターを呼び出して Intersection observer を作成し、閾値が一方向また他の方向に交差する度に実行されるコールバック関数を渡します。</p> + +<pre class="brush: js">let options = { + root: document.querySelector('#scrollArea'), + rootMargin: '0px', + threshold: 1.0 +} + +let observer = new IntersectionObserver(callback, options);</pre> + +<p>1.0 の閾値は、 <code>root</code> オプションで指定された要素内でターゲットが100%表示された時にコールバックが呼び出されることを意味しています。</p> + +<h4 id="Intersection_observer_options" name="Intersection_observer_options">Intersection observer のオプション</h4> + +<p>{{domxref("IntersectionObserver.IntersectionObserver", "IntersectionObserver()")}} コンストラクタに渡された <code>options</code> オブジェクトは、オブザーバーのコールバックが呼び出される状況を制御し、以下のフィールドがあります:</p> + +<dl> + <dt><code>root</code></dt> + <dd>ターゲットが見えるかどうかを確認するためのビューポートとして使用される要素です。指定されなかった場合、もしくは <code>null</code> の場合はデフォルトでブラウザーのビューポートが使用されます。</dd> + <dt><code>rootMargin</code></dt> + <dd>root の周りのマージンです。CSS {{cssxref("margin")}} プロパティに似た値を持つことができます。例えば、"<code>10px 20px 30px 40px"</code> (top, right, bottom, left) のようなものです。この値はパーセント値にすることができます。この一連の値は、交差を計算する前にルート要素の範囲のボックスの各辺を拡大または縮小させることができます。既定ではすべてゼロです。</dd> + <dt><code>threshold</code></dt> + <dd>単一の数値もしくは数値の配列で、オブザーバーのコールバックを実行するターゲットがどのくらいの割合で見えているかを示します。 50% 通過したときのみ検出する場合は値 0.5 を使用します。 25% を超える度にコールバックを実行する場合は、 [0, 0.25, 0.5, 0.75, 1] という配列を指定します。既定値は 0 です (つまり、1ピクセルでも表示されるとコールバックが実行されます)。1.0 の値は全てのピクセルが見えるようになるまで、閾値をまたいだとみなされないことを意味します。</dd> +</dl> + +<h4 id="Targeting_an_element_to_be_observed" name="Targeting_an_element_to_be_observed">監視される要素をターゲットにする</h4> + +<p>オブザーバーを作成した後は、監視するターゲット要素を与える必要があります。</p> + +<pre class="brush: js">var target = document.querySelector('#listItem'); +observer.observe(target); +</pre> + +<p>ターゲットが <code>IntersectionObserver</code> に指定された閾値を満たす度にコールバックが呼び出されます。コールバックは {{domxref("IntersectionObserverEntry")}} オブジェクトのリストとオブザーバーを受け取ります。</p> + +<pre class="brush: js">let callback = (entries, observer) => { + entries.forEach(entry => { + // Each entry describes an intersection change for one observed + // target element: + // entry.boundingClientRect + // entry.intersectionRatio + // entry.intersectionRect + // entry.isIntersecting + // entry.rootBounds + // entry.target + // entry.time + }); +}; +</pre> + +<p>コールバックはメインスレッドで実行される点に注意してください。可能な限り早く動作する必要があります。もし時間を要する処理であるなら、 {{domxref("Window.requestIdleCallback()")}} を使ったほうがいいでしょう。</p> + +<p>また <code>root</code> オプションを指定した場合、target はルート要素の子要素でなければなりません。</p> + +<h3 id="How_intersection_is_calculated" name="How_intersection_is_calculated">交差の計算方法</h3> + +<p>Intersection Observer API によって考慮される領域は全て矩形です。不規則に整形された要素は、要素全てを囲む最小の矩形で占有しているとみなされます。同様に、要素の可視部分が矩形ではない場合、要素が交差する矩形は要素全ての可視部分を含む最小の矩形であると解釈されます。</p> + +<p>{{domxref("IntersectionObserverEntry")}} オブジェクトによって提供される様々なプロパティがどのように交差を表現しているかを知るともっと役に立つでしょう。</p> + +<h4 id="The_intersection_root_and_root_margin" name="The_intersection_root_and_root_margin">交差するルートと root margin</h4> + +<p>要素とその入れ物との交差を監視するには、入れ物をまずは知る必要があります。ここでの入れ物とは<strong>交差ルート</strong>もしくは<strong>ルート要素</strong>です。これは監視される要素の親要素となる文書内の特定の要素になるか、文書のビューポートを入れ物として使用する際は <code>null</code> になるかいずれかになります。</p> + +<p><strong><dfn>ルート交差矩形</dfn></strong>はターゲットをチェックするために使用される矩形です。この矩形は次のように決まります。</p> + +<ul> + <li>交差ルートが暗黙のルート (すなわち最上位の {{domxref("Document")}}) である場合、ルート交差矩形はビューポートの矩形になります。</li> + <li>交差ルートのあふれた部分が切り取られていた場合、ルート交差矩形はルート要素のコンテンツ領域になります。</li> + <li>それ以外の場合は、ルート交差矩形は交差ルートのクライアント矩形 ({{domxref("Element.getBoundingClientRect", "getBoundingClientRect()")}} を呼び出して返されるもの) です。</li> +</ul> + +<p>交差するルートとして使用される矩形は、<strong>ルートマージン</strong> <code>rootMargin</code> を {{domxref("IntersectionObserver")}} の作成時に設定することで調整することが可能です。 <code>rootMargin</code> の値は交差するルートの境界線各辺にオフセット追加定義して、最終的な交差のルートの境界線を作成します (コールバックが実行された際には {{domxref("IntersectionObserverEntry.rootBounds")}} で取得できるものです)。</p> + +<h4 id="Thresholds" name="Thresholds">閾値</h4> + +<p>Intersection Observer API はターゲット要素がどのくらい見えているのか微細な変化を全て知らせるのではなく、<ruby><strong>閾値</strong><rp> (</rp><rt>thresholds</rt><rp>) </rp></ruby>を使用します。オブザーバーを作成する際に、表示されるターゲット要素がどの程度見えているかのパーセンテージを表す1つ以上の数値を指定できます。API はこれらの閾値を超えて見えたかどうかの変更のみを知らせます。</p> + +<p>例えば、ターゲット要素が25%見える度に通知を受けたい場合は、オブザーバーを作成する際の閾値のリストとして [0, 0.25, 0.5, 0.75, 1] という配列を指定します。変更の通知を受ける時にコールバック関数に渡された {{domxref("IntersectionObserverEntry")}} の {{domxref("IntersectionObserverEntry.isIntersecting", "isIntersecting")}} プロパティの値をチェックすることで、変更が感知された方向 (つまり要素が見えたかどうかを) 判断することが出来ます。<code>isIntersecting</code> が <code>true</code> であれば、ターゲットは閾値を超えて少なくとも見るようになったということですし、<code>false</code> であればターゲットは指定した閾値では表示されなくなったということです。</p> + +<p>閾値の仕組みを感じ取るには、下のボックスをスクロールして見てください。その中にある各色のボックスには四隅全てにパーセンテージが表示されています。そのため、入れ物をスクロールする時にこれらのパーセンテージが変化することが分かります。各ボックスには異なる閾値がセットされています:</p> + +<ul> + <li>最初のボックスは可視点の各パーセンテージ値がセットされています; つまり{{domxref("IntersectionObserver.thresholds")}} の配列は <code>[0.00, 0.01, 0.02, ..., 0.99, 1.00]</code> となります。</li> + <li>2つ目のボックスは50%を指定した一つの閾値しか持ちません。</li> + <li>3つ目のボックスは10%見える毎の閾値を持っています (0%, 10%, 20%...)</li> + <li>最後のボックスは25%の閾値です。</li> +</ul> + +<div class="hidden"> +<h5 id="Threshold_example">Threshold example</h5> + +<pre class="brush: html"><template id="boxTemplate"> + <div class="sampleBox"> + <div class="label topLeft"></div> + <div class="label topRight"></div> + <div class="label bottomLeft"></div> + <div class="label bottomRight"></div> + </div> +</template> + +<main> + <div class="contents"> + <div class="wrapper"> + </div> + </div> +</main></pre> + +<pre class="brush: css">.contents { + position: absolute; + width: 700px; + height: 1725px; +} + +.wrapper { + position: relative; + top: 600px; +} + +.sampleBox { + position: relative; + left: 175px; + width: 150px; + background-color: rgb(245, 170, 140); + border: 2px solid rgb(201, 126, 17); + padding: 4px; + margin-bottom: 6px; +} + +#box1 { + height: 200px; +} + +#box2 { + height: 75px; +} + +#box3 { + height: 150px; +} + +#box4 { + height: 100px; +} + +.label { + font: 14px "Open Sans", "Arial", sans-serif; + position: absolute; + margin: 0; + background-color: rgba(255, 255, 255, 0.7); + border: 1px solid rgba(0, 0, 0, 0.7); + width: 3em; + height: 18px; + padding: 2px; + text-align: center; +} + +.topLeft { + left: 2px; + top: 2px; +} + +.topRight { + right: 2px; + top: 2px; +} + +.bottomLeft { + bottom: 2px; + left: 2px; +} + +.bottomRight { + bottom: 2px; + right: 2px; +} +</pre> + +<pre class="brush: js">let observers = []; + +startup = () => { + let wrapper = document.querySelector(".wrapper"); + + // Options for the observers + + let observerOptions = { + root: null, + rootMargin: "0px", + threshold: [] + }; + + // An array of threshold sets for each of the boxes. The + // first box's thresholds are set programmatically + // since there will be so many of them (for each percentage + // point). + + let thresholdSets = [ + [], + [0.5], + [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], + [0, 0.25, 0.5, 0.75, 1.0] + ]; + + for (let i=0; i<=1.0; i+= 0.01) { + thresholdSets[0].push(i); + } + + // Add each box, creating a new observer for each + + for (let i=0; i<4; i++) { + let template = document.querySelector("#boxTemplate").content.cloneNode(true); + let boxID = "box" + (i+1); + template.querySelector(".sampleBox").id = boxID; + wrapper.appendChild(document.importNode(template, true)); + + // Set up the observer for this box + + observerOptions.threshold = thresholdSets[i]; + observers[i] = new IntersectionObserver(intersectionCallback, observerOptions); + observers[i].observe(document.querySelector("#" + boxID)); + } + + // Scroll to the starting position + + document.scrollingElement.scrollTop = wrapper.firstElementChild.getBoundingClientRect().top + window.scrollY; + document.scrollingElement.scrollLeft = 750; +} + +intersectionCallback = (entries) => { + entries.forEach((entry) => { + let box = entry.target; + let visiblePct = (Math.floor(entry.intersectionRatio * 100)) + "%"; + + box.querySelector(".topLeft").innerHTML = visiblePct; + box.querySelector(".topRight").innerHTML = visiblePct; + box.querySelector(".bottomLeft").innerHTML = visiblePct; + box.querySelector(".bottomRight").innerHTML = visiblePct; + }); +} + +startup(); +</pre> +</div> + +<p>{{EmbedLiveSample("Threshold_example", 500, 500)}}</p> + +<h4 id="Clipping_and_the_intersection_rectangle" name="Clipping_and_the_intersection_rectangle">クリッピングと交差矩形</h4> + +<p>ブラウザーは次のように最終的な交差矩形を計算します。これはすべて行われることですが、交差がいつ発生するかを正確に把握するために、これらの手順を理解すると役立ちます。</p> + +<ol> + <li>ターゲット要素の境界矩形 (つまり、要素を構成するすべてのコンポーネントの境界ボックスを完全に囲む最小の矩形) は、ターゲットに対して {{domxref("Element.getBoundingClientRect", "getBoundingClientRect()")}} を呼び出すことによって取得されます。これは、交差する矩形の最大の大きさです。残りの手順では、交差しない部分を削除します。</li> + <li>ターゲットの直接の親ブロックから始まり、外側に向かって移動し、それぞれの包含ブロックのクリッピングが (存在すれば) 交差する長方形に適用されます。ブロックのクリッピングは、2つのブロックの交差と、 {{cssxref("overflow")}} プロパティで (存在すれば) 指定されたクリッピングモードに基づいて決定されます。 <code>overflow</code> に <code>visible</code> 以外を設定すると、クリッピングが行われます。</li> + <li>包含する要素の1つがネストされた閲覧コンテキストのルートである場合 ({{HTMLElement("iframe")}} に含まれる文書など)、交差する矩形は含まれているコンテキストのビューポートで切り取られ、コンテナー群を通して上方に再帰的にコンテナーの包含ブロックを続けます。ですから、最上位の <code><iframe></code> に到達したら、交差矩形はフレームのビューポートに切り取られ、フレームの親要素が次のブロックとなり、交差ルートに向けて再帰が行われます。</li> + <li>上方への再帰が交差ルートに達すると、結果の矩形が交差ルートの座標空間に対応付けられます。</li> + <li>結果の矩形はそれから<a href="/ja/docs/Web/API/Intersection_Observer_API#root-intersection-rectangle">ルート交差矩形</a>と交差することで更新されます。</li> + <li>この矩形は、最終的に、ターゲットの {{domxref("document")}} の座標空間に対応付けられます。</li> +</ol> + +<h3 id="Intersection_change_callbacks" name="Intersection_change_callbacks">交差状態の変化のコールバック</h3> + +<p>ターゲット要素がルート要素内で見えている範囲が可視量の閾値を通過したとき、 {{domxref("IntersectionObserver")}} オブジェクトのコールバックが実行されます。コールバックは、入力引数として交差したすべての閾値を示す <code>IntersectionObserverEntry</code> オブジェクトの配列を、また参照として <code>IntersectionObserver</code> オブジェクト自身を受け取ります。</p> + +<p>閾値のリスト内のそれぞれの項目は、通過した閾値を説明する {{domxref("IntersectionObserverEntry")}} オブジェクトです。つまり、それぞれの項目は指定された要素がルート要素とどれだけ交差したのか、要素が交差したと言えるのかどうか、推移が発生した方向を示します。</p> + +<p>以下のコードスニペットは、要素がルート要素と交差していない状態から少なくとも75%が交差した状態まで推移した回数を数え続けて表示します。</p> + +<pre class="brush: js">intersectionCallback(entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + let elem = entry.target; + + if (entry.intersectionRatio >= 0.75) { + intersectionCounter++; + } + } + }); +} +</pre> + +<h2 id="Interfaces" name="Interfaces">インターフェイス</h2> + +<dl> + <dt>{{domxref("IntersectionObserver")}}</dt> + <dd>Intersection Observer API のプライマリーなインターフェイスです。同一の交差設定に対して任意の数のターゲット要素を監視するオブザーバーを作成し管理するためのメソッドを提供します。各オブザーバーは1つ以上のターゲット要素と共通の親要素、もしくは最上位の{{domxref("Document")}} の {{Glossary('viewport')}} との交差における変化を非同期的に監視することが出来ます。この親要素もしくはビューポートは<strong>ルート</strong>と呼ばれます。</dd> + <dt>{{domxref("IntersectionObserverEntry")}}</dt> + <dd>スクロールにおける変化の特定の瞬間において、ターゲット要素とルートとなる入れ物との交差を表現します。このタイプのオブジェクトは2つの方法でのみ得られます: <code>IntersectionObserver</code> コールバックへの入力として、または {{domxref("IntersectionObserver.takeRecords()")}} を呼び出すことによって、の2つです。</dd> +</dl> + +<h2 id="A_simple_example" name="A_simple_example">単純な例</h2> + +<p>この単純な例では、ターゲット要素の色と透明度を要素の可視性で変化させます。<a href="/ja/docs/Web/API/Intersection_Observer_API/Timing_element_visibility">Intersection Observer API を利用した時間の絡んだ要素の可視性</a>では、要素 (例えば広告など) セットがユーザーに表示される時間を測定し、統計を記録したり要素を更新したりしてその情報にユーザーどう反応したかを示す、より拡張性の高い具体例を見ることが出来るようでしょう。</p> + +<h3 id="HTML">HTML</h3> + +<p>この例における HTML は非常に短く、主な要素はターゲットとなるボックス (IDは <code>"box"</code> としました) とボックス内のコンテンツです。</p> + +<pre class="brush: html"><div id="box"> + <div class="vertical"> + Welcome to <strong>The Box!</strong> + </div> +</div></pre> + +<h3 id="CSS">CSS</h3> + +<p>この例では CSS はあまり重要ではありません。要素をレイアウトし{{cssxref("background-color")}} と {{cssxref("border")}} 属性を <a href="/ja/docs/Web/CSS/CSS_Transitions">CSS トランジション</a>に適用させます。CSS transitions は要素の変化に多少変化が起きることを確認するために使用します。</p> + +<pre class="brush: css">#box { + background-color: rgba(40, 40, 190, 255); + border: 4px solid rgb(20, 20, 120); + transition: background-color 1s, border 1s; + width: 350px; + height: 350px; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +.vertical { + color: white; + font: 32px "Arial"; +} + +.extra { + width: 350px; + height: 350px; + margin-top: 10px; + border: 4px solid rgb(20, 20, 120); + text-align: center; + padding: 20px; +}</pre> + +<h3 id="JavaScript">JavaScript</h3> + +<p>最後に、Intersection Observer API を使って何が出来るか、 JavaScript のコードを見ていきましょう。</p> + +<h4 id="Setting_up" name="Setting_up">セットアップ</h4> + +<p>まずは、いくつかの変数を準備してオブザーバーをインストールする必要があります。</p> + +<pre class="brush: js">const numSteps = 20.0; + +let boxElement; +let prevRatio = 0.0; +let increasingColor = "rgba(40, 40, 190, ratio)"; +let decreasingColor = "rgba(190, 40, 40, ratio)"; + +// Set things up +window.addEventListener("load", (event) => { + boxElement = document.querySelector("#box"); + + createObserver(); +}, false);</pre> + +<p>セットアップした定数と変数は下記の通りです。</p> + +<dl> + <dt><code>numSteps</code></dt> + <dd>視認率が0.0から1.0の間にどのくらいの数の閾値を設定するか示す定数です。</dd> + <dt><code>prevRatio</code></dt> + <dd>この変数は閾値を超えた最後の視認率を記録するために使用します。これはターゲット要素が大体見えるようになったかどうかを調べることが出来ます。</dd> + <dt><code>increasingColor</code></dt> + <dd>視認率が増加している時にターゲット要素に適用する色を定義する文字列です。文字列の中の "比率" という単語はターゲット要素の現在の視認率に置き換えられ、要素が色を変化させるだけでなく不透明になるにつれて透明度が増していきます。</dd> + <dt><code>decreasingColor</code></dt> + <dd>同様に、視認率が減少していく時に適用する色を定義する文字列です。</dd> +</dl> + +<p>{{domxref("EventTarget.addEventListener", "Window.addEventListener()")}} を呼び出して{{event("load")}} イベントのリスンを開始します。ページロードが完了すると、{{domxref("Document.querySelector", "querySelector()")}} を使用して ID <code>"box"</code> 要素への参照を取得し <code>createObserver()</code> メソッドを呼び出して Intersection Observer の設定・インストール処理を開始します。</p> + +<h4 id="Creating_the_intersection_observer" name="Creating_the_intersection_observer">Intersection Observer の作成</h4> + +<p><code>createObserver()</code> メソッドは新しい {{domxref("IntersectionObserver")}} を作成し、ターゲット要素の監視を開始するためにページが完全にロードされてから呼び出されます。</p> + +<pre class="brush: js">function createObserver() { + let observer; + + let options = { + root: null, + rootMargin: "0px", + threshold: buildThresholdList() + }; + + observer = new IntersectionObserver(handleIntersect, options); + observer.observe(boxElement); +}</pre> + +<p>この関数ではオブザーバーの設定を含む <code>options</code> オブジェクトを設定することから始めます。ドキュメントビューポートに対してターゲット要素がどのくらい見えているかという変化を監視したいので、<code>root</code> は <code>null</code> にします。マージンは必要がないので、マージンオフセットである <code>rootMargin</code> 設定は "0px" と指定しています。これによって、オブザーバーは追加された (もしくは差し引かれた) スペースがなくてもターゲット要素の境界とビューポートの境界の交差点がどう変化するのか監視を開始することが出来ます。</p> + +<p>視認率の閾値のリストである、<font face="consolas, Liberation Mono, courier, monospace"><span style="background-color: rgba(220, 220, 220, 0.5);">threshold</span></font>は関数 <code>buildThresholdList()</code> によって構成されます。閾値のリストは、この例ではプラグラムによって計算されています。その数が意図的に調整可能だからです。</p> + +<p><code>options</code> が用意できたら、新しいオブザーバーを作成、つまり{{domxref("IntersectionObserver.IntersectionObserver", "IntersectionObserver()")}} のコンストラクタを呼び出して、閾値をまたいだ際に呼ばれる関数 <code>handleIntersect()</code> を指定し、オプションを指定します。次に、返されたオブザーバーに対して {{domxref("IntersectionObserver.observe", "observe()")}} を呼び出し、必要なターゲット要素を渡します。</p> + +<p><code>observer.observe()</code> をそれぞれの要素に対して呼び出すことにより、ビューポートに対して交差し変化しているかを複数の要素から監視することが出来ます。</p> + +<h4 id="Building_the_array_of_threshold_ratios" name="Building_the_array_of_threshold_ratios">閾値比率の配列を組み立てる</h4> + +<p>閾値のリストを作成する <code>buildThresholdList()</code> 関数は次のようになります。</p> + +<pre class="brush: js">function buildThresholdList() { + let thresholds = []; + let numSteps = 20; + + for (let i=1.0; i<=numSteps; i++) { + let ratio = i/numSteps; + thresholds.push(ratio); + } + + thresholds.push(0); + return thresholds; +}</pre> + +<p>これは 1 と <code>numSteps</code> の間の各整数 <code>i</code> に対して、値 <code>i/numSteps</code> を閾値の配列に入れることで、それぞれが 0.0 と 1.0 の間の比率である閾値の配列を作成しています。また、0 を配列に含めます。デフォルトの <code>numSteps</code> (20) が指定された結果、以下の閾値のリストが表示れます。</p> + +<table class="standard-table"> + <tbody> + <tr> + <th>#</th> + <th>Ratio</th> + <th>#</th> + <th>Ratio</th> + </tr> + <tr> + <th>1</th> + <td>0.05</td> + <th>11</th> + <td>0.55</td> + </tr> + <tr> + <th>2</th> + <td>0.1</td> + <th>12</th> + <td>0.6</td> + </tr> + <tr> + <th>3</th> + <td>0.15</td> + <th>13</th> + <td>0.65</td> + </tr> + <tr> + <th>4</th> + <td>0.2</td> + <th>14</th> + <td>0.7</td> + </tr> + <tr> + <th>5</th> + <td>0.25</td> + <th>15</th> + <td>0.75</td> + </tr> + <tr> + <th>6</th> + <td>0.3</td> + <th>16</th> + <td>0.8</td> + </tr> + <tr> + <th>7</th> + <td>0.35</td> + <th>17</th> + <td>0.85</td> + </tr> + <tr> + <th>8</th> + <td>0.4</td> + <th>18</th> + <td>0.9</td> + </tr> + <tr> + <th>9</th> + <td>0.45</td> + <th>19</th> + <td>0.95</td> + </tr> + <tr> + <th>10</th> + <td>0.5</td> + <th>20</th> + <td>1.0</td> + </tr> + </tbody> +</table> + +<p>もちろん、閾値の配列をハードコードすることは可能ですし、よくやりがちなことです。しかし、この例では設定を追加することで粒度を調整する余地が残っています。</p> + +<h4 id="Handling_intersection_changes" name="Handling_intersection_changes">交差の変換の処理</h4> + +<p>ブラウザーはターゲット要素 (このケースでは <code>"box"</code> というIDを持つ要素です) が表示されているか、またはどのくらい見えているかという比率が、閾値のリストにある値の1つをまたぐことを検出して、<code>handleIntersect()</code> が呼び出されます:</p> + +<pre class="brush: js">function handleIntersect(entries, observer) { + entries.forEach((entry) => { + if (entry.intersectionRatio > prevRatio) { + entry.target.style.backgroundColor = increasingColor.replace("ratio", entry.intersectionRatio); + } else { + entry.target.style.backgroundColor = decreasingColor.replace("ratio", entry.intersectionRatio); + } + + prevRatio = entry.intersectionRatio; + }); +}</pre> + +<p>リストである <code>entries</code> 内にある {{domxref("IntersectionObserverEntry")}} について、entry の {{domxref("IntersectionObserverEntry.intersectionRatio", "intersectionRatio")}} が上昇しているかを調べます。上昇していればターゲットの {{cssxref("background-color")}} に <code>increasingColor</code> (<code>"rgba(40, 40, 190, ratio)"</code> だったことを思い出してください) の値をセットし、その際にその中にある "ratio" という文字列を entry が持つ <code>intersectionRatio</code> と置き換えます。その結果、色が変更されるだけでなく、ターゲット要素の透明度も変更されます。交差する比率が下がるに連れて、背景色のアルファ値が下がりより透明度の高い要素となります。</p> + +<p>同様に、 <code>intersectionRatio</code> が下がっている場合は <code>decreasingColor</code> を文字列として使用し "ratio" という文字列を <code>intersectionRatio</code> でもって置き換えたあとに、要素の <code>background-color</code> として適用します。</p> + +<p>最後に、交差する割合が上がっているか下がっているかを追跡するために、変数 <code>prevRatio</code> に現在の比率を代入しておきます。</p> + +<h3 id="Result" name="Result">結果</h3> + +<p>以下がその結果内容です。ページを上下にスクロールして、ボックスの外観がどう変化するかを確認してみましょう。</p> + +<p>{{EmbedLiveSample('A_simple_example', 400, 400)}}</p> + +<p>より応用的な例は<a href="/ja/docs/Web/API/Intersection_Observer_API/Timing_element_visibility">Timing element visibility with the Intersection Observer API</a> のセクションを見てください。</p> + +<h2 id="Specifications" name="Specifications">仕様書</h2> + +<table class="standard-table"> + <thead> + <tr> + <th scope="col">仕様書</th> + <th scope="col">状態</th> + <th scope="col">備考</th> + </tr> + </thead> + <tbody> + <tr> + <td>{{SpecName('IntersectionObserver')}}</td> + <td>{{Spec2('IntersectionObserver')}}</td> + <td></td> + </tr> + </tbody> +</table> + +<h2 id="Browser_compatibility" name="Browser_compatibility">ブラウザーの対応</h2> + +<div class="hidden">このページの互換性一覧表は構造化データから生成されています。データに協力していただけるのであれば、 <a class="external" href="https://github.com/mdn/browser-compat-data">https://github.com/mdn/browser-compat-data</a> をチェックアウトしてプルリクエストを送信してください。</div> + +<p>{{Compat("api.IntersectionObserver")}}</p> + +<h2 id="See_also" name="See_also">関連情報</h2> + +<ul> + <li><a href="https://github.com/w3c/IntersectionObserver">Intersection Observer polyfill</a></li> + <li><a href="/ja/docs/Web/API/Intersection_Observer_API/Timing_element_visibility">Timing element visibility with the Intersection Observer API</a></li> + <li>{{domxref("IntersectionObserver")}} と {{domxref("IntersectionObserverEntry")}}</li> +</ul> diff --git a/files/ja/web/api/intersection_observer_api/timing_element_visibility/index.html b/files/ja/web/api/intersection_observer_api/timing_element_visibility/index.html new file mode 100644 index 0000000000..42f9db568a --- /dev/null +++ b/files/ja/web/api/intersection_observer_api/timing_element_visibility/index.html @@ -0,0 +1,565 @@ +--- +title: Intersection Observer API を使用したタイミング要素の可視性 +slug: Web/API/Intersection_Observer_API/Timing_element_visibility +tags: + - API + - Intersection Observer + - Intersection Observer API + - チュートリアル + - 中級 + - 例 +translation_of: Web/API/Intersection_Observer_API/Timing_element_visibility +--- +<div>{{DefaultAPISidebar("Intersection Observer API")}}</div> + +<p><a href="/ja/docs/Web/API/Intersection_Observer_API">Intersection Observer API</a> は、{{domxref("Document")}} 自体を含む共有祖先ノードまたは要素によって、対象の要素が多かれ少なかれ不明瞭になったときに、非同期的に通知されることを容易にします。 <span class="seoSummary">この記事では、ページのコンテンツ間に多数の広告が散在している模擬ブログを作成し、その後 Intersection Observer API を使用して、各広告がユーザに見える時間を追跡します。表示時間の1分を超える広告は、新しい広告と置き換えられます。</span></p> + +<p>この例の多くの側面は実際の使用状況とは一致しません (特に記事はすべて同じテキストを持ち、データベースからは読み込まれません。また、配列から選択された単なるテキストのみの広告がほんの一握りです )、これは API の十分な理解を提供して、あなたのサイトに Intersection Observer API をどのように適用するかを素早く学ぶべきです。</p> + +<p>この例で、広告の視認性を追跡するという概念が使用されているのには理由があります。Web 上での広告の Flash やその他のスクリプトの最も一般的な用途の1つは、課金と収入のために各広告が表示される期間を記録することです。Intersection Observer API がなければ、個々の広告の間隔やタイムアウト、またはページを遅くする傾向があるその他の手法を使用して完了します。この API を使用することで、ブラウザによる効率化が図られ、パフォーマンスへの影響が大幅に軽減されます。</p> + +<p>始めましょう!</p> + +<div id="fullpage_example"> +<h2 id="サイト構造:HTML">サイト構造:HTML</h2> + +<p>The site's structure is not too complicated. We'll be using <a href="/en-US/docs/Web/CSS/CSS_Grid_Layout">CSS Grid</a> to style and lay out the site, so we can be pretty straightforward here:</p> + +<pre class="brush: html"><div class="wrapper"> + <header> + <h1>A Fake Blog</h1> + <h2>Showing Intersection Observer in action!</h2> + </header> + + <aside> + <nav> + <ul> + <li><a href="#link1">A link</a></li> + <li><a href="#link2">Another link</a></li> + <li><a href="#link3">One more link</a></li> + </ul> + </nav> + </aside> + + <main> + </main> +</div></pre> + +<p>This is the framework for the entire site. At the top is the site's header region, contained within a {{HTMLElement("header")}} block. Below that, we define the site's sidebar as a list of links within an {{HTMLElement("aside")}} block.</p> + +<p>Finally comes the main body. We start with an empty {{HTMLElement("main")}} element here. This box will be populated using script later.</p> + +<h2 id="CSS_によるサイトのスタイル設定">CSS によるサイトのスタイル設定</h2> + +<p>With the structure of the site defined, we turn to the styling for the site. Let's look at the style for each component of the page individually.</p> + +<h3 id="基礎">基礎</h3> + +<p>We provide styles for the {{HTMLElement("body")}} and {{HTMLElement("main")}} elements to define the site's background as well as the grid the various parts of the site will be placed in.</p> + +<pre class="brush: css">body { + font-family: "Open Sans", "Arial", "Helvetica", sans-serif; + background-color: aliceblue; +} + +.wrapper { + display: grid; + grid-template-columns: auto minmax(min-content, 1fr); + grid-template-rows: auto minmax(min-content, 1fr); + max-width: 700px; + margin: 0 auto; + background-color: aliceblue; +}</pre> + +<p>The site's {{HTMLElement("body")}} is configured here to use one of a number of common sans-serif fonts, and to use <code>"aliceblue"</code> as the background color. Then the <code>"wrapper"</code> class is defined; it wraps the entire blog, including the header, sidebar, and body content (articles and ads).</p> + +<p>The wrapper establishes a CSS grid with two columns and two rows. The first column (sized automatically based on its content) is used for the sidebar and the second column (which will be used for body content) is sized to be at least the width of the contents of the column and at most all remaining available space.</p> + +<p>The first row will be used specially for the site header. The rows are sized the same way as the columns: the first one is automatically sized and the one uses the remaining space, but at least enough space to provide room for all elements within it.</p> + +<p>The wrapper's width is fixed at 700px so that it will fit in the available space when presented inline on MDN below.</p> + +<h3 id="ヘッダー">ヘッダー</h3> + +<p>The header is fairly simple, since for this example all it contains is some text. Its style looks like this:</p> + +<pre class="brush: css">header { + grid-column: 1 / -1; + grid-row: 1; + background-color: aliceblue; +}</pre> + +<p>{{cssxref("grid-row")}} is set to 1, since we want the header to be placed in the top row of the site's grid. More interesting is our use of {{cssxref("grid-column")}} here; here we specify that we want the column to start in the first column and ends in the first column past the last grid line—in other words, the header spans across all of the columns within the grid. Perfect for our needs.</p> + +<h3 id="サイドバー">サイドバー</h3> + +<p>Our sidebar is used to present links to other pages on the site. None of them work in our example here, but they exist to help with the presentation of a blog-like experience. The sidebar is represented using an {{HTMLElement("aside")}} element, and is styled as follows:</p> + +<pre class="brush: css">aside { + grid-column: 1; + grid-row: 2; + background-color: cornsilk; + padding: 5px 10px; +} + +aside ul { + padding-left: 0; +} + +aside ul li { + list-style: none; +} + +aside ul li a { + text-decoration: none; +}</pre> + +<p>The most important thing to note here is that the {{cssxref("grid-column")}} is set to 1, to place the sidebar on the left-hand side of the screen. If you change this to -1, it will appear on the right (although some other elements will need some adjustments made to their margins to get the spacing just right). The {{cssxref("grid-row")}} is set to 2, to place it alongside the site body.</p> + +<h3 id="コンテンツ本体">コンテンツ本体</h3> + +<p>Speaking of the site's body: the main content of the site is kept in a {{HTMLElement("main")}} element. The following style is applied to that:</p> + +<pre class="brush: css">main { + grid-column: 2; + grid-row: 2; + margin: 0; + margin-left: 16px; + font-size: 16px; +}</pre> + +<p>The primary feature here is that the grid position is set to place the body content in column 2, row 2.</p> + +<h3 id="記事">記事</h3> + +<p>Each article is contained in an {{HTMLElement("article")}} element, styled like this:</p> + +<pre class="brush: css">article { + background-color: white; + padding: 6px; +} + +article:not(:last-child) { + margin-bottom: 8px; +} + +article h2 { + margin-top: 0; +}</pre> + +<p>This creates article boxes with a white background which float atop the blue background, with a small margin around the article. Every article which isn't the last item in the container has an 8px bottom margin to space things apart.</p> + +<h3 id="広告">広告</h3> + +<p>Finally, the ads have the following initial styling. Individual ads may customize the style somewhat, as we'll see later.</p> + +<pre class="brush: css">.ad { + height: 96px; + padding: 6px; + border-color: #555; + border-style: solid; + border-width: 1px; +} + +.ad:not(:last-child) { + margin-bottom: 8px; +} + +.ad h2 { + margin-top: 0; +} + +.ad div { + position: relative; + float: right; + padding: 0 4px; + height: 20px; + width: 120px; + font-size: 14px; + bottom: 30px; + border: 1px solid black; + background-color: rgba(255, 255, 255, 0.5); +}</pre> + +<p>There's nothing magic in here. It's fairly basic CSS.</p> + +<h2 id="JavaScript_と連携させる">JavaScript と連携させる</h2> + +<p>That brings us to the JavaScript code which makes everything work. Let's start with the global variables:</p> + +<pre class="brush: js">let contentBox; + +let nextArticleID = 1; +let visibleAds = new Set(); +let previouslyVisibleAds = null; + +let adObserver; +let refreshIntervalID = 0;</pre> + +<p>These are used as follows:</p> + +<dl> + <dt><code>contentBox</code></dt> + <dd>A reference to the {{HTMLElement("main")}} element's {{domxref("HTMLElement")}} object in the DOM. This is where we'll insert the articles and ads.</dd> + <dt><code>nextArticleID</code></dt> + <dd>Each article is given a unique ID number; this variable tracks the next ID to use, starting with 1.</dd> + <dt><code>visibleAds</code></dt> + <dd>A {{jsxref("Set")}} which we'll use to track the ads currently visible on the screen.</dd> + <dt><code>previouslyVisibleAds</code></dt> + <dd>Used to temporarily store the list of visible ads while the document is not visible (for example, if the user has tabbed to another page).</dd> + <dt><code>adObserver</code></dt> + <dd>Will hold our {{domxref("IntersectionObserver")}} used to track the intersection between the ads and the <code><main></code> element's bounds.</dd> + <dt><code>refreshIntervalID</code></dt> + <dd>Used to store the interval ID returned by {{domxref("WindowOrWorkerGlobalScope.setInterval", "setInterval()")}}. This interval will be used to trigger our periodic refreshes of the ads' content.</dd> +</dl> + +<h3 id="セットアップ">セットアップ</h3> + +<p>To set things up, we run the <code>startup()</code> function below when the page loads:</p> + +<pre class="brush: js">window.addEventListener("load", startup, false); + +function startup() { + contentBox = document.querySelector("main"); + + document.addEventListener("visibilitychange", handleVisibilityChange, false); + + let observerOptions = { + root: null, + rootMargin: "0px", + threshold: [0.0, 0.75] + }; + + adObserver = new IntersectionObserver(intersectionCallback, + observerOptions); + + buildContents(); + refreshIntervalID = window.setInterval(handleRefreshInterval, 1000); +}</pre> + +<p>First, a reference to the content wrapping {{HTMLElement("main")}} element is obtained, so we can insert our content into it. Then we set up an event listener for the {{event("visibilitychange")}} event. This event is sent when the document becomes hidden or visible, such as when the user switches tabs in their browser. The Intersection Observer API doesn't take this into account when detecting intersection, since intersection isn't affected by page visibility. Therefore, we need to pause our timers while the page is tabbed out; hence this event listener.</p> + +<p>Next we set up the options for the {{domxref("IntersectionObserver")}} which will monitor target elements (ads, in our case) for intersection changes relative to the document. The options are configured to watch for intersections with the document's viewport (by setting <code>root</code> to <code>null</code>). We have no margins to extend or contract the intersection root's rectangle; we want to match the boundaries of the document's viewport exactly for intersection purposes. And the <code>threshold</code> is set to an array containing the values 0.0 and 0.75; this will cause our callback to execute whenever a targeted element becomes completely obscured or first starts to become unobscured (intersection ratio 0.0) or passes through 75% visible in either direction (intersection ratio 0.75).</p> + +<p>The observer, <code>adObserver</code>, is created by calling <code>IntersectionObserver</code>'s constructor, passing in the callback function, <code>intersectionCallback</code>, and our options.</p> + +<p>We then call a function <code>buildContents()</code>, which we'll define later to actually generate and insert into the document the articles and ads we want to present.</p> + +<p>Finally, we set up an interval which triggers once a second to handle any necessary refreshing. We need a one second refresh since we're displaying timers in all visible ads for the purposes of this example. You may not need an interval at all, or you might do it differently or using a different time interval.</p> + +<h3 id="ドキュメントの可視性の変更の処理">ドキュメントの可視性の変更の処理</h3> + +<p>Let's take a look at the handler for the {{event("visibilitychange")}} event. Our script receives this event when the document itself becomes visible or invisible. The most important scenario here is when the user switches tabs. Since Intersection Observer only cares about the intersection between the targeted elements and the intersection root, and not the tab's visibility (which is a different issue entirely), we need to use the <a href="/en-US/docs/Web/API/Page_Visibility_API">Page Visibility API</a> to detect these tab switches and disable our timers for the duration.</p> + +<pre class="brush: js">function handleVisibilityChange() { + if (document.hidden) { + if (!previouslyVisibleAds) { + previouslyVisibleAds = visibleAds; + visibleAds = []; + previouslyVisibleAds.forEach(function(adBox) { + updateAdTimer(adBox); + adBox.dataset.lastViewStarted = 0; + }); + } + } else { + previouslyVisibleAds.forEach(function(adBox) { + adBox.dataset.lastViewStarted = performance.now(); + }); + visibleAds = previouslyVisibleAds; + previouslyVisibleAds = null; + } +}</pre> + +<p>Since the event itself doesn't state whether the document has switched from visible to invisible or vice-versa, the {{domxref("document.hidden")}} property is checked to see if the document is not currently visible. Since it's theoretically possible to get called multiple times, we only proceed if we haven't already paused the timers and saved the visibility states of the existing ads.</p> + +<p>To pause the timers, all we need to do is remove the ads from the set of visible ads (<code>visibleAds</code>) and mark them as inactive. To do so, we begin by saving the set of visible ads into a variable known as <code>previouslyVisibleAds</code> to be sure we can restore them when the user tabs back into the document, and we then empty the <code>visibleAds</code> set so they won't be treated as visible. Then, for each of the ads that are being suspended, we call our <code>updateAdTimer()</code> function, which handles updating the ad's total visible time counter, then we set their <code>dataset.lastViewStarted</code> property to 0, which indicates that the tab's timer isn't running.</p> + +<p>If the document has just become visible, we reverse this process: first we go through <code>previouslyVisibleAds</code> and set each one's <code>dataset.lastViewStarted</code> to the current document's time (in milliseconds since the document was created) using the {{domxref("Performance.now", "performance.now()")}} method. Then we set <code>visibleAds</code> back to <code>previouslyVisibleAds</code> and set the latter to <code>null</code>. Now the ads are all restarted, and configured to know that they became visible at the current time, so that they will not add up the duration of time the page was tabbed away the next time they're updated.</p> + +<h3 id="交差点の変更の処理">交差点の変更の処理</h3> + +<p>Once per pass through the browser's event loop, each {{domxref("IntersectionObserver")}} checks to see if any of its target elements have passed through any of the observer's intersection ratio thresholds. For each observer, a list of targets that have done so is compiled, and sent to the observer's callback as an array of {{domxref("IntersectionObserverEntry")}} objects. Our callback, <code>intersectionCallback()</code>, looks like this:</p> + +<pre class="brush: js">function intersectionCallback(entries) { + entries.forEach(function(entry) { + let adBox = entry.target; + + if (entry.isIntersecting) { + if (entry.intersectionRatio >= 0.75) { + adBox.dataset.lastViewStarted = entry.time; + visibleAds.add(adBox); + } + } else { + visibleAds.delete(adBox); + if ((entry.intersectionRatio === 0.0) && (adBox.dataset.totalViewTime >= 60000)) { + replaceAd(adBox); + } + } + }); +}</pre> + +<p>As previously mentioned, the {{domxref("IntersectionObserver")}} callback receives as input an array of all of the observer's targeted elements which have become either more or less visible than one of the intersection observer ratios. We iterate over each of those entries—which are of type {{domxref("IntersectionObserverEntry")}}. If the target element is intersecting with the root, we know it has just transitioned from the obscured state to the visible state. If it's become at least 75% visible, then we consider the ad visible and we start the timer by setting the ad's <code>dataset.lastViewStarted</code> attribute to the transition time in {{domxref("IntersectionObserverEntry.time", "entry.time")}}, then add the ad to the set <code>visibleAds</code> so we know to process it as time goes by.</p> + +<p>If the ad has transitioned to the not-intersecting state, we remove the ad from the set of visible ads. Then we have one special behavior: we look to see if {{domxref("IntersectionObserverEntry.intersectionRatio", "entry.ratio")}} is 0.0; if it is, that means the element has become totally obscured. If that's the case, and the ad has been visible for at least a minute total, we call a function we'll create called <code>replaceAd()</code> to replace the existing ad with a new one. This way, the user sees a variety of ads over time, but the ads are only replaced while they can't be seen, resulting in a smooth experience.</p> + +<h3 id="定期的なアクションの処理">定期的なアクションの処理</h3> + +<p>Our interval handler, <code>handleRefreshInterval()</code>, is called about once per second courtesy of the call to {{domxref("WindowOrWorkerGlobalScope.setInterval", "setInterval()")}} made in the <code>startup()</code> function {{anch("Setting up", "described above")}}. Its main job is to update the timers every second and schedule a redraw to update the timers we'll be drawing within each ad.</p> + +<pre class="brush: js">function handleRefreshInterval() { + let redrawList = []; + + visibleAds.forEach(function(adBox) { + let previousTime = adBox.dataset.totalViewTime; + updateAdTimer(adBox); + + if (previousTime != adBox.dataset.totalViewTime) { + redrawList.push(adBox); + } + }); + + if (redrawList.length) { + window.requestAnimationFrame(function(time) { + redrawList.forEach(function(adBox) { + drawAdTimer(adBox); + }); + }); + } +}</pre> + +<p>The array <code>redrawList</code> will be used to keep a list of all the ads which need to be redrawn during this refresh cycle, since it may not be exactly the same as the elapsed time due to system activity or because you've set the interval to something other than every 1000 milliseconds.</p> + +<p>Then, for each of the visible ads, we save the value of <code>dataset.totalViewTime</code> (the total number of milliseconds the ad has currently been visible, as of the last time it was updated) and then call <code>updateAdTimer()</code> to update the time. If it's changed, then we push the ad onto the <code>redrawList</code> so we know it needs to be updated during the next animation frame.</p> + +<p>Finally, if there's at least one element to redraw, we use {{domxref("window.requestAnimationFrame", "requestAnimationFrame()")}} to schedule a function that will redraw each element in the <code>redrawList</code> during the next animation frame.</p> + +<h3 id="広告の視認性タイマーを更新する">広告の視認性タイマーを更新する</h3> + +<p>Previously (see {{anch("Handling document visibility changes")}} and {{anch("Handling periodic actions")}}), we've seen that when we need to update an ad's "total visible time" counter, we call a function named <code>updateAdTimer()</code> to do so. This function takes as an input an ad's {{domxref("HTMLDivElement")}} object. Here it is:</p> + +<pre class="brush: js">function updateAdTimer(adBox) { + let lastStarted = adBox.dataset.lastViewStarted; + let currentTime = performance.now(); + + if (lastStarted) { + let diff = currentTime - lastStarted; + + adBox.dataset.totalViewTime = parseFloat(adBox.dataset.totalViewTime) + diff; + } + + adBox.dataset.lastViewStarted = currentTime; +}</pre> + +<p>To track an element's visible time, we use two custom data attributes (see {{htmlattrxref("data-*")}}) on every ad:</p> + +<dl> + <dt><code>lastViewStarted</code></dt> + <dd>The time in milliseconds, relative to the time at which the document was created, at which the ad's visibility count was last updated, or the ad last became visible. 0 if the ad was not visible as of the last time it was checked.</dd> + <dt><code>totalViewTime</code></dt> + <dd>The total number of milliseconds the ad has been visible.</dd> +</dl> + +<p>These are accessed through each ad's {{domxref("HTMLElement.dataset")}} attribute, which provides a {{domxref("DOMStringMap")}} mapping each custom attribute's name to its value. The values are strings, but we can convert those to numbers easily enough—in fact, JavaScript generally does it automatically, although we'll have one instance where we have to do it ourselves.</p> + +<p>We start by fetching the time at which the ad's previous visibility status check time (<code>adBox.dataset.lastViewStarted</code>) into a local variable named <code>lastStarted</code>. We also get the current time-since-creation value using {{domxref("Performance.now", "performance.now()")}} into <code>currentTime</code>.</p> + +<p>If <code>lastStarted</code> is non-zero—meaning the timer is currently running, we compute the difference between the current time and the start time to determine the number of milliseconds the timer has been visible since the last time it became visible. This is added to the current value of the ad's <code>totalViewTime</code> to bring the total up to date. Note the use of {{jsxref("parseFloat()")}} here; because these values are strings, JavaScript tries to do a string concatenation instead of addition without it.</p> + +<p>Finally, the last-viewed time for the ad is updated to the current time. This is done whether the ad was running when this function was called or not; this causes the ad's timer to always be running when this function returns. This makes sense because this function is only called if the ad is visible, even if it's just now become visible.</p> + +<h3 id="広告のタイマーを描画する">広告のタイマーを描画する</h3> + +<p>Inside each ad, for demonstration purposes, we draw the current value of its <code>totalViewTime</code>, converted into minutes and seconds. This is handled by passing the ad's element into the <code>drawAdTimer()</code> function:</p> + +<pre class="brush: js">function drawAdTimer(adBox) { + let timerBox = adBox.querySelector(".timer"); + let totalSeconds = adBox.dataset.totalViewTime / 1000; + let sec = Math.floor(totalSeconds % 60); + let min = Math.floor(totalSeconds / 60); + + timerBox.innerText = min + ":" + sec.toString().padStart(2, "0"); +}</pre> + +<p>This code finds the ad's timer using its ID, <code>"timer"</code>, and computes the number of seconds elapsed by dividing the ad's <code>totalViewTime</code> by 1000. Then it calculates the number of minutes and seconds elapsed before setting the timer's {{domxref("Node.innerText", "innerText")}} to a string representing that time in the form m:ss. The {{jsxref("String.padStart()")}} method is used to ensure that the number of seconds is padded out to two digits if it's less than 10.</p> + +<h3 id="ページコンテンツの構築">ページコンテンツの構築</h3> + +<p>The <code>buildContents()</code> function is called by the {{anch("Setting up", "startup code")}} to select and insert into the document the articles and ads to be presented:</p> + +<pre class="brush: js">let loremIpsum = "<p>Lorem ipsum dolor sit amet, consectetur adipiscing" + + " elit. Cras at sem diam. Vestibulum venenatis massa in tincidunt" + + " egestas. Morbi eu lorem vel est sodales auctor hendrerit placerat" + + " risus. Etiam rutrum faucibus sem, vitae mattis ipsum ullamcorper" + + " eu. Donec nec imperdiet nibh, nec vehicula libero. Phasellus vel" + + " malesuada nulla. Aliquam sed magna aliquam, vestibulum nisi at," + + " cursus nunc.</p>"; + +function buildContents() { + for (let i=0; i<5; i++) { + contentBox.appendChild(createArticle(loremIpsum)); + + if (!(i % 2)) { + loadRandomAd(); + } + } +} +</pre> + +<p>The variable <code>loremIpsum</code> contains the text we'll use for the body of all of our articles. Obviously in the real world, you'd have some code to pull articles from a database or the like, but this does the job for our purposes. Every article uses the same text; you could of course change that easily enough.</p> + +<p><code>buildContents()</code> creates a page with five articles. Following every odd-numbered article, an ad is "loaded" and inserted into the page. Articles are inserted into the content box (that is, the {{HTMLElement("main")}} element that contains all the site content) after being created using a method called <code>createArticle()</code>, which we'll look at next.</p> + +<p>The ads are created using a function called <code>loadRandomAd()</code>, which both creates the ad and inserts it into the page. We'll see later that this same function can also replace an existing ad, but for now, we're simply appending ads to the existing content.</p> + +<h3 id="記事を作成する">記事を作成する</h3> + +<p>To create the {{HTMLElement("article")}} element for an article (as well as all of its contents), we use the <code>createArticle()</code> function, which takes as input a string which is the full text of the article to add to the page.</p> + +<pre class="brush: js">function createArticle(contents) { + let articleElem = document.createElement("article"); + articleElem.id = nextArticleID; + + let titleElem = document.createElement("h2"); + titleElem.id = nextArticleID; + titleElem.innerText = "Article " + nextArticleID + " title"; + articleElem.appendChild(titleElem); + + articleElem.innerHTML += contents; + nextArticleID +=1 ; + + return articleElem; +}</pre> + +<p>First, the <code><article></code> element is created and its ID is set to the unique value <code>nextArticleID</code> (which starts at 1 and goes up for each article). Then we create and append an {{HTMLElement("h2")}} element for the article title and then we append the HTML from <code>contents</code> to that. Finally, <code>nextArticleID</code> is incremented (so that the next element gets a new unique ID) and we return the new <code><article></code> element to the caller.</p> + +<h3 id="広告を作成する">広告を作成する</h3> + +<p>The <code>loadRandomAd()</code> function simulates loading an ad and adding it to the page. If you don't pass a value for <code>replaceBox</code>, a new element is created to contain the ad; the ad is then appended to the page. if you specify a <code>replaceBox</code>, that box is treated as an existing ad element; instead of creating a new one, the existing element is changed to contain the new ad's style, content, and other data. This avoids the risk of lengthy layout work being done when you update the ad, which could happen if you first delete the old element then insert a new one.</p> + +<pre class="brush: js">function loadRandomAd(replaceBox) { + let ads = [ + { + bgcolor: "#cec", + title: "Eat Green Beans", + body: "Make your mother proud—they're good for you!" + }, + { + bgcolor: "aquamarine", + title: "MillionsOfFreeBooks.whatever", + body: "Read classic literature online free!" + }, + { + bgcolor: "lightgrey", + title: "3.14 Shades of Gray: A novel", + body: "Love really does make the world go round..." + }, + { + bgcolor: "#fee", + title: "Flexbox Florist", + body: "When life's layout gets complicated, send flowers." + } + ]; + let adBox, title, body, timerElem; + + let ad = ads[Math.floor(Math.random()*ads.length)]; + + if (replaceBox) { + adObserver.unobserve(replaceBox); + adBox = replaceBox; + title = replaceBox.querySelector(".title"); + body = replaceBox.querySelector(".body"); + timerElem = replaceBox.querySelector(".timer"); + } else { + adBox = document.createElement("div"); + adBox.className = "ad"; + title = document.createElement("h2"); + body = document.createElement("p"); + timerElem = document.createElement("div"); + adBox.appendChild(title); + adBox.appendChild(body); + adBox.appendChild(timerElem); + } + + adBox.style.backgroundColor = ad.bgcolor; + + title.className = "title"; + body.className = "body"; + title.innerText = ad.title; + body.innerHTML = ad.body; + + adBox.dataset.totalViewTime = 0; + adBox.dataset.lastViewStarted = 0; + + timerElem.className="timer"; + timerElem.innerText = "0:00"; + + if (!replaceBox) { + contentBox.appendChild(adBox); + } + + adObserver.observe(adBox); +}</pre> + +<p>First is the array <code>ads</code>. This array contains the data needed to create each ad. We have four here to choose from at random. In a real-world scenario, of course, the ads would come from a database or, more likely, an advertising service from which you fetch ads using an API. However, our needs are simple: each ad is represented by an object with three properties: a background color (<code>bgcolor</code>), a title (<code>title</code>), and a body text string (<code>body</code>).</p> + +<p>Then we define several variables:</p> + +<dl> + <dt><code>adBox</code></dt> + <dd>This will be set to the element that represents the ad. For new ads being appended to the page, this is created using {{domxref("Document.createElement()")}}. When replacing an existing ad, this is set to the specified ad element (<code>replaceBox</code>).</dd> + <dt><code>title</code></dt> + <dd>Will hold the {{HTMLElement("h2")}} element representing the ad's title.</dd> + <dt><code>body</code></dt> + <dd>Will hold the {{HTMLElement("p")}} representing the ad's body text.</dd> + <dt><code>timerElem</code></dt> + <dd>Will hold the {{HTMLElement("div")}} element which contains the time the ad has been visible so far.</dd> +</dl> + +<p>A random ad is selected by computing <code>Math.floor(Math.random() * ads.length)</code>; the result is a value between 0 and one less than the number of ads. The corresponding ad is now known as <code>adBox</code>.</p> + +<p>If a value is specified for <code>replaceBox</code>, we use that as the ad element. To do so, we begin by ending observation of the element by calling {{domxref("IntersectionObserver.unobserve()")}}. Then the local variables for each of the elements that comprise an ad: the ad box itself, the title, the body, and the timer box, are all set to the corresponding elements in the existing ad.</p> + +<p>If no value is specified for replaceBox, we create a new ad element. The ad's new {{HTMLElement("div")}} element is created and its properties established by setting its class name to <code>"ad"</code>. Next, the ad title element is created, along with the body and the visibility timer; these are an {{HTMLElement("h2")}}, a {{HTMLElement("p")}}, and a {{HTMLElement("div")}} element, respectively. These elements are appended to the <code>adBox</code> element.</p> + +<p>After that, the code paths converge once again. The ad's background color is set to the value specified in the new ad's record, and elements' classes and contents are set appropriately as well.</p> + +<p>Next, it's time to set up the custom data properties to track the ad's visibility data by setting <code>adBox.dataset.totalViewTime</code> and <code>adBox.dataset.lastViewStarted</code> to 0.</p> + +<p>Finally, we set the ID of the <code><div></code> which will show the timer we'll present in the ad to show how long it's been visible, giving it the class <code>"timer"</code>. The initial text is set to "0:00", to represent the starting time of 0 minutes and 0 seconds, and it's appended to the ad.</p> + +<p>If we're not replacing an existing ad, we need to append the element to the content area of the page using {{domxref("Node.appendChild", "Document.appendChild()")}}. If we're replacing an ad, it's already there, with its contents replaced with the new ad's. Then we call the {{domxref("IntersectionObserver.observe", "observe()")}} method on our Intersection Observer, <code>adObserver</code>, to start watching the ad for changes to its intersection with the viewport. From now on, any time the ad becomes 100% obscured or even a single pixel becomes visible, or the ad passes through 75% visible in one way or another, the {{anch("Handling intersection changes", "observer's callback")}} is executed.</p> + +<h3 id="既存の広告を置き換える">既存の広告を置き換える</h3> + +<p>Our {{anch("Handling intersection changes", "observer's callback")}} keeps an eye out for ads which become 100% obscured and have a total visible time of at least one minute. When that happens, the <code>replaceAd()</code> function is called with that ad's element as an input, so that the old ad can be replaced with a new one.</p> + +<pre class="brush: js">function replaceAd(adBox) { + let visibleTime; + + updateAdTimer(adBox); + + visibleTime = adBox.dataset.totalViewTime + console.log(" Replacing ad: " + adBox.querySelector("h2").innerText + " - visible for " + visibleTime) + + loadRandomAd(adBox); +}</pre> + +<p><code>replaceAd()</code> begins by calling <code>updateAdTimer()</code> on the existing ad, to ensure that its timer is up-to-date. This ensures that when we read its <code>totalViewTime</code>, we see the exact final value for how long the ad was visible to the user. We then report that data; in this case, by logging it to console, but in the real world, you'd submit the information to an ad service's API or save it into a database.</p> + +<p>Then we load a new ad by calling <code>{{anch("Creating an ad", "loadRandomAd()")}}</code>, specifying the ad to be replaced as an input parameter. As we saw previously, <code>loadRandomAd()</code> will replace an existing ad with content and data corresponding to a new ad, if you specify an existing ad's element as an input parameter.</p> + +<p>The new ad's element object is returned to the caller in case it's needed.</p> +</div> + +<h2 id="結果">結果</h2> + +<p>結果のページは次のようになります。スクロールして試してみて、視認性の変化が各広告のタイマーに与える影響を見てみましょう。 また、1分の視認性と、ドキュメントがバックグラウンドでタブ表示されている間にタイマーが一時停止すると、各広告が置き換えられます。</p> + +<p>{{EmbedLiveSample("fullpage_example", 750, 800)}}</p> + +<h2 id="あわせて参照">あわせて参照</h2> + +<ul> + <li><a href="/ja/docs/Web/API/Intersection_Observer_API">Intersection Observer API</a></li> + <li><a href="/ja/docs/Web/Guide/User_experience/Using_the_Page_Visibility_API">Page Visibility API</a></li> +</ul> |