2022 年 6 月 26 日

元素大小與捲動

有許多 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 顯示「溢位」的文字,這是正常的。

幾何

以下是幾何屬性的整體圖片

技術上來說,這些屬性的值是數字,但這些數字是「像素」,因此這些是像素量測。

讓我們從元素的外側開始探索屬性。

offsetParent、offsetLeft/Top

這些屬性很少需要,但它們仍然是「最外側」的幾何屬性,所以我們將從它們開始。

offsetParent 是瀏覽器在渲染期間用於計算座標的最接近祖先。

那是以下其中一個的最接近祖先

  1. CSS 定位(positionabsoluterelativefixedsticky),或
  2. <td><th><table>,或
  3. <body>.

屬性 offsetLeft/offsetTop 提供相對於 offsetParent 左上角的 x/y 座標。

在以下範例中,內部的 <div><main> 作為 offsetParent,而 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>

在下列情況下,offsetParent 會為 null

  1. 對於未顯示的元素(display:none 或不在文件中)。
  2. 對於 <body><html>
  3. 對於具有 position:fixed 的元素。

offsetWidth/Height

現在讓我們繼續了解元素本身。

這兩個屬性是最簡單的。它們提供元素的「外」寬度/高度。或者,換句話說,它的完整大小,包括邊框。

對於我們的範例元素

  • offsetWidth = 390 – 外寬度,可以計算為內部 CSS 寬度(300px)加上內距(2 * 20px)和邊框(2 * 25px)。
  • offsetHeight = 290 – 外高度。
對於未顯示的元素,幾何屬性為零/null

幾何屬性僅計算顯示的元素。

如果元素(或其任何祖先)具有 display:none 或不在文件中,則所有幾何屬性均為零(或 offsetParentnull)。

例如,當我們建立一個元素,但尚未將其插入文件中,或它(或其祖先)具有 display:none 時,offsetParentnull,而 offsetWidthoffsetHeight0

我們可以使用它來檢查元素是否隱藏,如下所示

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

請注意,對於在螢幕上但大小為零的元素,此類 isHidden 會傳回 true

clientTop/Left

在元素內部,我們有邊框。

為了測量它們,有屬性 clientTopclientLeft

在我們的範例中

  • clientLeft = 25 – 左邊框寬度
  • clientTop = 25 – 上邊框寬度

…但要精確地說,這些屬性不是邊框寬度/高度,而是內側相對於外側的相對座標。

差別在哪裡?

當文件從右到左(作業系統為阿拉伯語或希伯來語)時,就會變得明顯。然後,捲軸列不會在右側,而是在左側,然後 clientLeft 也會包含捲軸列寬度。

在這種情況下,clientLeft 將不是 25,而是加上捲軸列寬度 25 + 16 = 41

以下是以希伯來語為例

clientWidth/Height

這些屬性提供元素邊框內區域的大小。

它們包含內容寬度以及內邊距,但不包含捲軸列

在上面的圖片中,我們首先考慮clientHeight

沒有水平捲軸列,因此它正好是邊框內所有內容的總和:CSS 高度200px加上頂部和底部內邊距 (2 * 20px),總計240px

現在是clientWidth - 這裡的內容寬度不是300px,而是284px,因為16px被捲軸列佔用。因此,總和是284px加上左右內邊距,總計324px

如果沒有內邊距,則clientWidth/Height正好是內容區域,在邊框和捲軸列(如果有)內部。

因此,當沒有內邊距時,我們可以使用clientWidth/clientHeight來獲取內容區域大小。

scrollWidth/Height

這些屬性類似於clientWidth/clientHeight,但它們還包括捲動出去(隱藏)的部分

在上面的圖片中

  • scrollHeight = 723 - 是內容區域的完整內部高度,包括捲動出去的部分。
  • scrollWidth = 324 - 是完整的內部寬度,這裡沒有水平捲軸,因此它等於clientWidth

我們可以使用這些屬性將元素擴展到其完整寬度/高度。

像這樣

// expand the element to the full content height
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 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

按一下

1
2
3
4
5
6
7
8
9

scrollTop 設定為 0 或大值(例如 1e9)會分別使元素捲動到最上方/最下方。

不要從 CSS 取得寬度/高度

我們剛剛介紹了 DOM 元素的幾何屬性,可用於取得寬度、高度和計算距離。

但我們從 樣式和類別 章節中得知,我們可以使用 getComputedStyle 讀取 CSS 高度和寬度。

那麼,為什麼不使用 getComputedStyle 讀取元素的寬度,如下所示?

let elem = document.body;

alert( getComputedStyle(elem).width ); // show CSS width for elem

為什麼我們應該改用幾何屬性?原因有兩個

  1. 首先,CSS width/height 取決於另一個屬性:box-sizing,它定義了 CSS 寬度和高度的「意義」。出於 CSS 目的而變更 box-sizing 可能會導致此類 JavaScript 發生問題。

  2. 其次,CSS width/height 可能為 auto,例如對於內嵌元素

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

    從 CSS 的觀點來看,width:auto 完全正常,但在 JavaScript 中,我們需要計算中可用的精確大小(以 px 為單位)。因此,這裡的 CSS 寬度毫無用處。

還有一個原因:捲軸。有時,在沒有捲軸的情況下正常運作的程式碼會因為捲軸而出現錯誤,因為在某些瀏覽器中,捲軸會佔用內容的空間。因此,可用於內容的實際寬度小於 CSS 寬度。而 clientWidth/clientHeight 會將其考慮在內。

…但對於 getComputedStyle(elem).width,情況有所不同。有些瀏覽器(例如 Chrome)會傳回實際的內部寬度(減去捲軸),而有些瀏覽器(例如 Firefox)則會傳回 CSS 寬度(忽略捲軸)。這種跨瀏覽器差異就是不使用 getComputedStyle 而依賴幾何屬性的原因。

如果您的瀏覽器為捲軸保留空間(大多數 Windows 瀏覽器都會這麼做),則您可以在下方進行測試。

包含文字的元素具有 CSS width:300px

在桌上型 Windows 作業系統上,Firefox、Chrome 和 Edge 都會為捲軸保留空間。但 Firefox 顯示 300px,而 Chrome 和 Edge 顯示較小的值。這是因為 Firefox 傳回 CSS 寬度,而其他瀏覽器傳回「實際」寬度。

請注意,所描述的差異僅與從 JavaScript 讀取 getComputedStyle(...).width 有關,在視覺上一切都是正確的。

摘要

元素具有下列幾何屬性

  • offsetParent – 是最近定位的祖先或 tdthtablebody
  • offsetLeft/offsetTop – 相對於 offsetParent 左上角的座標。
  • offsetWidth/offsetHeight – 元素的「外」寬度/高度,包含邊框。
  • clientLeft/clientTop – 從左上角外角到左上角內角(內容 + 內距)的距離。對於由左至右的作業系統,它們總是左/上邊框的寬度。對於由右至左的作業系統,垂直捲軸在左側,因此 clientLeft 也包含其寬度。
  • clientWidth/clientHeight – 內容的寬度/高度,包含內距,但不包含捲軸。
  • scrollWidth/scrollHeight – 內容的寬度/高度,就像 clientWidth/clientHeight,但還包括元素中已捲動、不可見的部分。
  • scrollLeft/scrollTop – 元素中已捲動的頂部部分的寬度/高度,從其左上角開始。

除了 scrollLeft/scrollTop 之外,所有屬性都是唯讀的,如果已變更,則會使瀏覽器捲動元素。

任務

重要性:5

elem.scrollTop 屬性是從頂部捲動的部分的大小。如何取得底部捲動的大小(我們稱之為 scrollBottom)?

撰寫適用於任意 elem 的程式碼。

附註:請檢查您的程式碼:如果沒有捲動或元素已完全向下捲動,則應傳回 0

解決方案是

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

換句話說:(完整高度)減去(已捲動的頂部部分)減去(可見部分) – 這正是已捲動的底部部分。

重要性:3

撰寫傳回標準捲軸寬度的程式碼。

對於 Windows,它通常在 12px20px 之間變化。如果瀏覽器未保留任何空間給它(捲軸半透明地覆蓋在文字上,也會發生),則它可能是 0px

附註:程式碼應適用於任何 HTML 文件,不依賴於其內容。

若要取得捲軸寬度,我們可以建立一個有捲動但沒有邊框和內距的元素。

然後,其完整寬度 offsetWidth 和內部內容區域寬度 clientWidth 之間的差異將正好是捲軸

// create a div with the scroll
let div = document.createElement('div');

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

// must put it in the document, otherwise sizes will be 0
document.body.append(div);
let scrollWidth = div.offsetWidth - div.clientWidth;

div.remove();

alert(scrollWidth);
重要性:5

原始文件如下所示

球場中央的座標是什麼?

計算座標並將球放置在綠色球場的中央

  • 元素應該使用 JavaScript 移動,而不是 CSS。
  • 程式碼應該適用於任何球的大小(102030 像素)和任何球場的大小,而不受限於給定的值。

附註:當然,可以使用 CSS 置中,但這裡我們特別需要 JavaScript。之後我們會遇到其他主題和更複雜的情況,必須使用 JavaScript。這裡我們先做個「熱身運動」。

開啟沙盒進行任務。

球的 position:absolute。這表示它的 left/top 座標是從最近定位的元素測量而來,也就是 #field(因為它的 position: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。這會導致上述程式碼中的座標錯誤。

在第一次載入後,瀏覽器通常會快取影像,而在重新載入時,它會立即取得影像的大小。但在第一次載入時,ball.offsetWidth 的值是 0

我們應該透過將 width/height 新增到 <img> 來修正這個問題

<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 屬性始終相同:如果保留了捲軸列大小,則會減去。
教學課程地圖

留言

留言前請先閱讀...
  • 如果您有改進建議,請提交 GitHub 議題或提交 Pull Request,而不是留言。
  • 如果您無法理解文章中的某些內容,請詳細說明。
  • 要插入少量的程式碼,請使用 <code> 標籤,要插入多行程式碼,請將它們包覆在 <pre> 標籤中,要插入超過 10 行的程式碼,請使用沙盒 (plnkrjsbincodepen…)