2023年9月25日

要素サイズとスクローリング

要素の幅、高さや他のジオメトリの特徴を関する情報を読み取ることのできる JavaScript のプロパティがたくさんあります。

JavaScript では、要素を移動したり配置するときに、座標を正しく計算するためにしばしばそれらを必要とします。

サンプル要素

プロパティを示すサンプルの要素として、以下のものを使用します:

<div id="example">
  ...Text...
</div>
<style>
  #example {
    width: 300px;
    height: 200px;
    border: 25px solid #E8C48F;
    padding: 20px;
    overflow: auto;
  }
</style>

それはボーダー、パディングとスクローリングを持っています。特徴のフルセットです。要素自体の一部ではないため、マージンはありません。また、特別なプロパティはありません。

要素はこのように見えます:

サンドボックスでドキュメントを開く ことができます。

スクロールバーを気にしてください

上の図は、要素にスクロールバーがあるときの最も複雑なケースを示します。いくつかのブラウザ(すべてではありません)はコンテンツから取得することで、スクロールバーの領域を予約します。

したがって、スクロールバーがなければ、コンテンツの幅は 300px になりますが、スクロールバーの幅が 16px の場合(幅はデバイスやブラウザによって異なる場合があります)、 300-16 = 284px しか残っておらず、我々はそれを考慮する必要があります。そのため、このチャプターの例ではスクロールバーがあると仮定しています。もしスクロールバーがない場合には、物事は少しシンプルです。

padding-bottom はテキストで埋められるかもしれません

通常、パディングはイラストでは空白で表示されますが、要素に多くのテキストがあり、オーバーフローする場合、ブラウザは padding-bottom に “オーバーフローしている” テキストを表示します。例でそれを見ることができます。しかし特に明記されていない限り、パディングは依然としてそこにあります。

ジオメトリ

幅、高さや他のジオメトリを提供する要素プロパティは常に数値です。それらはピクセルと仮定されます。

ここに全体像があります:

それらは多くのプロパティがあり、1つの図でそれらすべてを表現するのは難しいです。が、それらの値は単純で理解しやすいものです。

要素の外側からそれらを調べてみましょう。

offsetParent, offsetLeft/Top

これらのプロパティは殆ど必要とされませんが、依然としてそれらは “最も外側の” ジオメトリプロパティなので、ここから始めましょう。

offsetParent は次のような、最も近い祖先です。:

  1. CSS-positioned(CSS位置) (positionabsolute, relative または fixed),
  2. もしくは <td>, <th>, <table>,
  3. もしくは <body>.

ほとんどの実践的なケースでは、offsetParent を使用して、最も近い CSS位置の祖先の取得出来ます。そして、offsetLeft/offsetTop はその左上隅への相対的な x/y 座標を提供します。

下の例では、内部の <div>offsetParent として <main> を持っており、offsetLeft/offsetTop はその左上隅(180)からシフトしています。:

<main style="position: relative" id="main">
  <article>
    <div id="example" style="position: absolute; left: 180px; top: 180px">...</div>
  </article>
</main>
<script>
  alert(example.offsetParent.id); // main
  alert(example.offsetLeft); // 180 (note: a number, not a string "180px")
  alert(example.offsetTop); // 180
</script>

offsetParentnull である場合がいくつかあります:

  1. 表示されていない要素(display:none またはドキュメント上にない)の場合
  2. <body><html> の場合
  3. position:fixed を持つ要素の場合

offsetWidth/Height

次に、要素自身へ移動しましょう。

これら2つのプロパティは最もシンプルなものです。これらは要素の “外部の” 幅/高さを提供します。もしくは、言い換えると、ボーダーを含むその要素の完全なサイズです。

サンプル要素の場合:

  • offsetWidth = 390 – 外部の幅で、内部のCSS幅(300px)とパディング(2*20px) とボーダー(2*25px) を足したものです。
  • offsetHeight = 290 – 外部の高さです。
表示されていない要素のジオメトリプロパティはゼロ/nullです

ジオメトリプロパティは表示されている要素に対してのみ計算されます。

もし要素(またはその祖先)が display:none を持っている、もしくはドキュメント上にない場合、それに応じてすべてのジオメトリプロパティはゼロもしくは null になります。

例えば、offsetParentnulloffsetWidth, offsetHeight0 です。

私たちは次のように、これを要素が隠されているかをチェックするための使う事ができます:

function isHidden(elem) {
  return !elem.offsetWidth && !elem.offsetHeight;
}

このような isHidden は、(空の <div> のように)画面上にあるがサイズがゼロの要素の場合に true を返します。

clientTop/Left

要素の内側にはボーダーがあります。

それらを測るため、clientTopclientLeft があります。

私たちの例では:

  • clientLeft = 25 – 左のボーダー幅
  • clientTop = 25 – トップのボーダー幅

…しかし正確には – それらはボーダーではなく、外側から内側の相対座標です。

違いは何でしょう?

ドキュメントが右から左のとき(OSがアラビア語またはヘブライ語のとき)、その違いが見えます。スクロールバーは右側にはなく左側にあります。clientLeft にはスクロールバーの幅も含まれます。

この場合、今回の例の clientLeft25 ではなく、スクロールバーの幅を含めたものになります 25+16=41:

clientWidth/Height

これらのプロパティは要素のボーダーの内側の領域のサイズを提供します。

パディングと一緒にコンテンツの幅を含みますが、スクロールバーは含まれません。:

上の図では、最初に clientHeight を考えましょう: これは評価するのが簡単です。水平スクロールバーがないので、ボーダーの内側の合計になります: CSS高さ 200px に top と bottom のパディング(2*20px) を加えた合計 240px です。

さて、clientWidth は – コンテンツ幅は 300px ではなく 284px です。なぜなら 16px はスクロールバーの幅だからです。したがって、合計は 284px に左右のパディングを足した 324px です。

もしパディングがない場合、clientWidth/Height は丁度ボーダーとスクロールバー(存在する場合)の内側のコンテンツ領域なります。

したがって、パディングがない場合には、コンテンツ領域のサイズを取得するために clientWidth/clientHeight を使うことができます。

scrollWidth/Height

  • プロパティ clientWidth/clientHeight は要素の可視部分のみを占めます。
  • プロパティ scrollWidth/scrollHeight はスクロールアウトされた(隠れた)部分も含めます:

上の図では:

  • scrollHeight = 723 – はスクロールアウト部分を含めたコンテンツ領域一杯の内部の高さです。
  • scrollWidth = 324 – は内部の全幅で、ここでは水平スクロールがないので、clientWidth と等しくなります。

要素の広さを、その一杯の幅/高さに広げるのにこれらのプロパティを使うことができます。

このように:

// 要素をコンテンツの高さ一杯に広げます
element.style.height = element.scrollHeight + 'px';

ボタンをクリックして要素を広げてください:

text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text

scrollLeft/scrollTop

プロパティ scrollLeft/scrollTop は隠された部分の 幅/高さ で、要素のスクロールアウトされた部分です。

下の図では、縦スクロールのブロックに対する scrollHeightscrollTop を見ることが出来ます。

つまり、scrollTop は “どのくらいスクロールされているか” です。

scrollLeft/scrollTop は変更可能です

ほとんどのジオメトリプロパティは読み取り専用ですが、scrollLeft/scrollTop は変更可能で、ブラウザは要素をスクロールさせます。

下の要素をクリックすると、コード elem.scrollTop+=10 を実行します。それは要素のコンテンツを 10px 下にスクロールします。

Click
Me
1
2
3
4
5
6
7
8
9

scrollTop0 または Infinity に設定すると、要素はそれぞれトップ/ボトムまでスクロールします。

CSS から幅/高さを取らないでください

私たちは、DOM要素のジオメトリプロパティを説明してきました。それらは通常、幅や高さを取得したり、距離を計算するために使われます。

しかし、チャプター スタイルとクラス でご存知の通り、CSSの幅/高さはgetComputedStyle を使って読み取る事ができます。

なので、次のようにして要素の幅を読んだらどうでしょう?

let elem = document.body;

alert( getComputedStyle(elem).width ); // 要素の CSS幅を表示します

なぜ代わりにジオメトリプロパティを使う必要があるのでしょうか?2つ理由があります:

  1. まず、CSS 幅/高さ は別のプロパティに依存しています: CSS の幅と高さが “何か” を定義する box-sizing です。CSS 目的のために box-sizing を変更すると、このような JavaScript が壊れる可能性があります。

  2. 次に、CSS width/heightauto の場合があります。例えば、インライン要素の場合です:

    <span id="elem">Hello!</span>
    
    <script>
      alert( getComputedStyle(elem).width ); // auto
    </script>

    CSSの立場からだと、width:auto は完全は正常ですが, JavaScript では計算で使える px での正確なサイズが必要です。そのため、ここでは CSS 幅は全く役に立ちません。

また、もう1つ理由があります: スクロールバーです。スクロールバーがない場合に上手く動作するコードは、スクロールバーがあると不具合を引き起こすことがあります。なぜなら、スクロールバーは、ブラウザによってはコンテンツからスペースを取るためです。従って、コンテンツのために本当に利用可能な幅は CSS 幅よりも 小さい です。そして、clientWidth/clientHeight はそれを考慮します。

…しかし、getComputedStyle(elem).width を使った状況では異なります。一部のブラウザ(e.g. Chrome) ではスクロールバーを引いた実際の内部幅を返し、別のいくつか(e.g. Firefox)は – CSS 幅を返します(スクロールバーを無視)。このようなクロスブラウザの差異が、getComputedStyle の利用ではなく、むしろジオメトリプロパティに頼るべき理由です。

もしあなたのブラウザがスクロールバーのスペースを確保するなら(Windows用のほとんどのブラウザがそうです)、下のサンプルでそれをテストできます。

テキストを持つ要素は CSS width:300x です。

デスクトップ Windows OS 上の Firefox, Chrome, Edge はすべてスクロールバーのためのスペースを確保します。しかし、ChromeとEdgeがより小さい値を表示する一方、Firefox は 300px を表示します。これは、Firefox が CSS 幅を返し、他のブラウザは “実際の” 幅を返すためです。

説明した違いは JavaScript から getComputedStyle(...).width を読むことについてのみであり、視覚的にはすべて正しいことに注意してください。

サマリ

要素は次のジオメトリプロパティを持っています:

  • offsetParent – は最も近い position 指定された祖先 もしくは td, th, table, body です。
  • offsetLeft/offsetTop – は offsetParent の左上端を基準とする座標です。
  • offsetWidth/offsetHeight – はボーダーを含む要素の “外部の” 幅/高さです。
  • clientLeft/clientTop – は左上の外側の角から左上の内側の角までの距離です。 左から右の OS では、常に左/上のボーダー幅です。 右から左へのOSの場合、縦スクロールバーは左にあります。したがって、 clientLeft もその幅を含みます。
  • clientWidth/clientHeight – はパディングを含むコンテンツの幅/高さですが、スクロールバーは含みません。
  • scrollWidth/scrollHeight – スクロールアウト部分を含むコンテンツの幅/高さです。パディングも含みますが、スクロールバーは含みません。
  • scrollLeft/scrollTop – 要素のスクロールアウトされた部分の幅/高さで、左上の角から距離です。

scrollLeft/scrollTop 以外のすべてのプロパティは読み取り専用です。scrollLeft/scrollTop を変更するとブラウザは要素をスクロールします。

タスク

重要性: 5

elem.scrollTop プロパティはトップからスクロールアウトした部分のサイズです。"scrollBottom" ーーボトムからのサイズはどうやったら取得できるでしょうか?

任意の elem で動作するコードを書いてください。

P.S. あなたのコードを確認してください: もしスクロールがない、もしくは要素が完全に下にスクロールしている場合、0 を返すべきです。

解決策は:

let scrollBottom = elem.scrollHeight - elem.scrollTop - elem.clientHeight;

つまり: (全高さ) – (スクロールアウトしたトップ部分) – (可視部分) – これは正確にスクロールアウトしたボトム部分です。

重要性: 3

標準のスクロールバーの幅を返すコードを書いてください。

Windowsでは、通常 12px20px の間で変化します。もしブラウザがスクロールバーのためのスペースを予約していない場合は、0px の可能性があります。

P.S. コードは任意のHTMLドキュメントで動作すべきで、コンテンツには依存しません。

スクロールバーの幅を取得するために、私たちはスクロールを持つがボーダーとパディングは含まない要素を作成することができます。

そして、その全幅 offsetWidth と内部コンテンツ領域幅 clientWidth の間の違いは、スクロールバーになります:

// スクロールを持つ div を作成します
let div = document.createElement('div');

div.style.overflowY = 'scroll';
div.style.width = '50px';
div.style.height = '50px';

// ドキュメントに置く必要があります、さもないとサイズは 0 です
document.body.append(div);
let scrollWidth = div.offsetWidth - div.clientWidth;

div.remove();

alert(scrollWidth);
重要性: 5

ソースのドキュメントは次のように見えます。:

フィールドの中心の座標は何でしょうか?

それを計算し、フィールドの中心にボールを置いてください。:

  • 要素は CSS ではなく JavaScript で移動させてください。
  • コードは任意のボールサイズ(10, 20, 30 px)、フィールドサイズで動作し、指定された値で束縛されないようにしてください。

P.S. 確かに、センタリングはCSSで行うことができますが、ここでは正確にJavaScriptが必要です。 さらに、私たちはJavaScriptを使用する必要がある他のトピックやより複雑な状況にも対応します。ここでは “ウォームアップ” を行います。

タスクのためのサンドボックスを開く

ボールは position:absolute を持っています。それは、その left/top 座標は最も近い position指定された要素、つまり #field から測定されることを意味します(なぜなら #fieldposition:relative を持っているため)。

座標は、フィールド内部の左上の角から始まります:

内部フィールドの幅/高さは clientWidth/clientHeight です。なので、フィールドの中央は、座標 (clientWidth/2, clientHeight/2) となります。

…しかし、ball.style.left/top をこのような値にセットした場合、ボール全体ではなく、ボールの左上の端が中心になります:

ball.style.left = Math.round(field.clientWidth / 2) + 'px';
ball.style.top = Math.round(field.clientHeight / 2) + 'px';

次のように見えます:

ボールの中心をフィールドの中心に揃えるために、ボールをその幅の半分だけ左に移動させ、その高さの半分だけ上に移動させる必要があります。:

ball.style.left = Math.round(field.clientWidth / 2 - ball.offsetWidth / 2) + 'px';
ball.style.top = Math.round(field.clientHeight / 2 - ball.offsetHeight / 2) + 'px';

注意: 落とし穴!

<img> が幅/高さを持たないとき、コードは確実に動作しません:

<img src="ball.png" id="ball">

ブラウザがイメージの幅/高さを知らないとき(タグ属性またはCSSから)、イメージの読み込みが終了するまで 0 と等しいと仮定します。

実際には、最初の読み込みの後、ブラウザは通常イメージをキャッシュし、次の読み込みではすぐにサイズを取得します。

しかし、最初の読み込み時、 ball.offsetWidth の値は 0 です。それは間違った座標に繋がります。

<img>width/height を追加し、それを修正する必要があります:

<img src="ball.png" width="40" height="40" id="ball">

…または CSS でサイズを与えます:

#ball {
  width: 40px;
  height: 40px;
}

サンドボックスで解答を開く

重要性: 5

getComputedStyle(elem).widthelem.clientWidth の間の違いはなんでしょうか?

少なくとも 3つの差異を挙げてください。多い方が良いです。

違い:

  1. clientWidth は数値で、getComputedStyle(elem).width は末尾に px がついた文字列を返します。
  2. getComputedStyle はインライン要素の場合、"auto" のような非数値の幅を返す場合があります。
  3. clientWidth は要素の内部のコンテンツ領域にパディングを加えたものである一方、CSS幅(標準の box-sizingの場合)は パディングなし の内部コンテンツ領域です。
  4. もしスクロールバーがあり、ブラウザはそのスペースを確保している場合、一部のブラウザは CSS幅からそのスペースを引き(コンテンツのためにはそれ以上使えないからです)、一部のブラウザは引きません。clientWidth プロパティは常に同じです: 確保されている場合、スクロールバーサイズは引かれます。
チュートリアルマップ

コメント

コメントをする前に読んでください…
  • 自由に記事への追加や質問を投稿をしたり、それらに回答してください。
  • 数語のコードを挿入するには、<code> タグを使ってください。複数行の場合は <pre> を、10行を超える場合にはサンドボックスを使ってください(plnkr, JSBin, codepen…)。
  • 記事の中で理解できないことがあれば、詳しく説明してください。