Javascript 坐標(biāo)

2023-02-17 10:54 更新

要移動(dòng)頁(yè)面的元素,我們應(yīng)該先熟悉坐標(biāo)。

大多數(shù) JavaScript 方法處理的是以下兩種坐標(biāo)系中的一個(gè):

  1. 相對(duì)于窗口 —— 類似于 position:fixed,從窗口的頂部/左側(cè)邊緣計(jì)算得出。
    • 我們將這些坐標(biāo)表示為 ?clientX/clientY?,當(dāng)我們研究事件屬性時(shí),就會(huì)明白為什么使用這種名稱來(lái)表示坐標(biāo)。
  2. 相對(duì)于文檔 —— 與文檔根(document root)中的 position:absolute 類似,從文檔的頂部/左側(cè)邊緣計(jì)算得出。
    • 我們將它們表示為 ?pageX/pageY?。

當(dāng)頁(yè)面滾動(dòng)到最開始時(shí),此時(shí)窗口的左上角恰好是文檔的左上角,它們的坐標(biāo)彼此相等。但是,在文檔移動(dòng)之后,元素的窗口相對(duì)坐標(biāo)會(huì)發(fā)生變化,因?yàn)樵卦诖翱谥幸苿?dòng),而元素在文檔中的相對(duì)坐標(biāo)保持不變。

在下圖中,我們?cè)谖臋n中取一點(diǎn),并演示了它滾動(dòng)之前(左)和之后(右)的坐標(biāo):



當(dāng)文檔滾動(dòng)了:

  • ?pageY? —— 元素在文檔中的相對(duì)坐標(biāo)保持不變,從文檔頂部(現(xiàn)在已滾動(dòng)出去)開始計(jì)算。
  • ?clientY? —— 窗口相對(duì)坐標(biāo)確實(shí)發(fā)生了變化(箭頭變短了),因?yàn)橥粋€(gè)點(diǎn)越來(lái)越靠近窗口頂部。

元素坐標(biāo):getBoundingClientRect

方法 elem.getBoundingClientRect() 返回最小矩形的窗口坐標(biāo),該矩形將 elem 作為內(nèi)建 DOMRect 類的對(duì)象。

主要的 DOMRect 屬性:

  • ?x/y? —— 矩形原點(diǎn)相對(duì)于窗口的 X/Y 坐標(biāo),
  • ?width/height? —— 矩形的 width/height(可以為負(fù))。

此外,還有派生(derived)屬性:

  • ?top/bottom? —— 頂部/底部矩形邊緣的 Y 坐標(biāo),
  • ?left/right? —— 左/右矩形邊緣的 X 坐標(biāo)。

如果你滾動(dòng)此頁(yè)面并重復(fù)點(diǎn)擊上面那個(gè)按鈕,你會(huì)發(fā)現(xiàn)隨著窗口相對(duì)按鈕位置的改變,其窗口坐標(biāo)(如果你垂直滾動(dòng)頁(yè)面,則為 y/top/bottom)也隨之改變。

下面這張是 elem.getBoundingClientRect() 的輸出的示意圖:


正如你所看到的,x/y 和 width/height 對(duì)矩形進(jìn)行了完整的描述??梢院苋菀椎貜乃鼈冇?jì)算出派生(derived)屬性:

  • ?left = x?
  • ?top = y?
  • ?right = x + width?
  • ?bottom = y + height?

請(qǐng)注意:

  • 坐標(biāo)可能是小數(shù),例如 ?10.5?。這是正常的,瀏覽器內(nèi)部使用小數(shù)進(jìn)行計(jì)算。在設(shè)置 ?style.left/top? 時(shí),我們不是必須對(duì)它們進(jìn)行舍入。
  • 坐標(biāo)可能是負(fù)數(shù)。例如滾動(dòng)頁(yè)面,使 ?elem? 現(xiàn)在位于窗口的上方,則 ?elem.getBoundingClientRect().top? 為負(fù)數(shù)。

為什么需要派生(derived)屬性?如果有了 ?x/y?,為什么還要還會(huì)存在 ?top/left??

從數(shù)學(xué)上講,一個(gè)矩形是使用其起點(diǎn) (x,y) 和方向向量 (width,height) 唯一定義的。因此,其它派生屬性是為了方便起見。

從技術(shù)上講,width/height 可能為負(fù)數(shù),從而允許“定向(directed)”矩形,例如代表帶有正確標(biāo)記的開始和結(jié)束的鼠標(biāo)選擇。

負(fù)的 width/height 值表示矩形從其右下角開始,然后向左上方“增長(zhǎng)”。

這是一個(gè)矩形,其 width 和 height 均為負(fù)數(shù)(例如 width=-200,height=-100):


正如你所看到的,在這個(gè)例子中,left/top 與 x/y 不相等。

但是實(shí)際上,elem.getBoundingClientRect() 總是返回正數(shù)的 width/height,這里我們提及負(fù)的 width/height 只是為了幫助你理解,為什么這些看起來(lái)重復(fù)的屬性,實(shí)際上并不是重復(fù)的。

IE 瀏覽器不支持 ?x/y?

由于歷史原因,IE 瀏覽器不支持 x/y 屬性。

因此,我們可以寫一個(gè) polyfill(在 DomRect.prototype 中添加一個(gè) getter),或者僅使用 top/left,因?yàn)閷?duì)于正值的 width/height 來(lái)說(shuō),它們和 x/y 一直是一樣的,尤其是對(duì)于 elem.getBoundingClientRect() 的結(jié)果。

坐標(biāo)的 right/bottom 與 CSS position 屬性不同

相對(duì)于窗口(window)的坐標(biāo)和 CSS position:fixed 之間有明顯的相似之處。

但是在 CSS 定位中,right 屬性表示距右邊緣的距離,而 bottom 屬性表示距下邊緣的距離。

如果我們?cè)倏匆幌律厦娴膱D片,我們可以看到在 JavaScript 中并非如此。窗口的所有坐標(biāo)都從左上角開始計(jì)數(shù),包括這些坐標(biāo)。

elementFromPoint(x, y)

對(duì) document.elementFromPoint(x, y) 的調(diào)用會(huì)返回在窗口坐標(biāo) (x, y) 處嵌套最多(the most nested)的元素。

語(yǔ)法如下:

let elem = document.elementFromPoint(x, y);

例如,下面的代碼會(huì)高亮顯示并輸出現(xiàn)在位于窗口中間的元素的標(biāo)簽:

let centerX = document.documentElement.clientWidth / 2;
let centerY = document.documentElement.clientHeight / 2;

let elem = document.elementFromPoint(centerX, centerY);

elem.style.background = "red";
alert(elem.tagName);

因?yàn)樗褂玫氖谴翱谧鴺?biāo),所以元素可能會(huì)因當(dāng)前滾動(dòng)位置而有所不同。

對(duì)于在窗口之外的坐標(biāo),?elementFromPoint? 返回 ?null?

方法 document.elementFromPoint(x,y) 只對(duì)在可見區(qū)域內(nèi)的坐標(biāo) (x,y) 起作用。

如果任何坐標(biāo)為負(fù)或者超過(guò)了窗口的 width/height,那么該方法就會(huì)返回 null。

在大多數(shù)情況下,這種行為并不是一個(gè)問(wèn)題,但是我們應(yīng)該記住這一點(diǎn)。

如果我們沒(méi)有對(duì)其進(jìn)行檢查,可能就會(huì)發(fā)生下面這個(gè)典型的錯(cuò)誤:

let elem = document.elementFromPoint(x, y);
// 如果坐標(biāo)恰好在窗口外,則 elem = null
elem.style.background = ''; // Error!

用于 “fixed” 定位

大多數(shù)時(shí)候,我們需要使用坐標(biāo)來(lái)確定某些內(nèi)容的位置。

想要在某元素附近展示內(nèi)容,我們可以使用 getBoundingClientRect 來(lái)獲取這個(gè)元素的坐標(biāo),然后使用 CSS position 以及 left/top(或 right/bottom)。

例如,下面的函數(shù) createMessageUnder(elem, html) 在 elem 下顯示了消息:

let elem = document.getElementById("coords-show-mark");

function createMessageUnder(elem, html) {
  // 創(chuàng)建 message 元素
  let message = document.createElement('div');
  // 在這里最好使用 CSS class 來(lái)定義樣式
  message.style.cssText = "position:fixed; color: red";

  // 分配坐標(biāo),不要忘記 "px"!
  let coords = elem.getBoundingClientRect();

  message.style.left = coords.left + "px";
  message.style.top = coords.bottom + "px";

  message.innerHTML = html;

  return message;
}

// 用法:
// 在文檔中添加 message 保持 5 秒
let message = createMessageUnder(elem, 'Hello, world!');
document.body.append(message);
setTimeout(() => message.remove(), 5000);

我們可以修改代碼以在元素左側(cè),右側(cè)或下面顯示消息,也可以應(yīng)用 CSS 動(dòng)畫來(lái)營(yíng)造“淡入淡出”效果等。這很簡(jiǎn)單,因?yàn)槲覀冇性撛厮凶鴺?biāo)和大小。

但是請(qǐng)注意一個(gè)重要的細(xì)節(jié):滾動(dòng)頁(yè)面時(shí),消息就會(huì)從按鈕流出。

原因很顯然:message 元素依賴于 position:fixed,因此當(dāng)頁(yè)面滾動(dòng)時(shí),它仍位于窗口的同一位置。

要改變這一點(diǎn),我們需要使用基于文檔(document)的坐標(biāo)和 position:absolute 樣式。

文檔坐標(biāo)

文檔相對(duì)坐標(biāo)從文檔的左上角開始計(jì)算,而不是窗口。

在 CSS 中,窗口坐標(biāo)對(duì)應(yīng)于 position:fixed,而文檔坐標(biāo)與頂部的 position:absolute 類似。

我們可以使用 position:absolute 和 top/left 來(lái)把某些內(nèi)容放到文檔中的某個(gè)位置,以便在頁(yè)面滾動(dòng)時(shí),元素仍能保留在該位置。但是我們首先需要正確的坐標(biāo)。

這里沒(méi)有標(biāo)準(zhǔn)方法來(lái)獲取元素的文檔坐標(biāo)。但是寫起來(lái)很容易。

這兩個(gè)坐標(biāo)系統(tǒng)通過(guò)以下公式相連接:

  • ?pageY? = ?clientY? + 文檔的垂直滾動(dòng)出的部分的高度。
  • ?pageX? = ?clientX? + 文檔的水平滾動(dòng)出的部分的寬度。

函數(shù) getCoords(elem) 將從 elem.getBoundingClientRect() 獲取窗口坐標(biāo),并向其中添加當(dāng)前滾動(dòng):

// 獲取元素的文檔坐標(biāo)
function getCoords(elem) {
  let box = elem.getBoundingClientRect();

  return {
    top: box.top + window.pageYOffset,
    right: box.right + window.pageXOffset,
    bottom: box.bottom + window.pageYOffset,
    left: box.left + window.pageXOffset
  };
}

如果在上面的示例中,我們將其與 position:absolute 一起使用,則在頁(yè)面滾動(dòng)時(shí),消息仍停留在元素附近。

修改后的 createMessageUnder 函數(shù):

function createMessageUnder(elem, html) {
  let message = document.createElement('div');
  message.style.cssText = "position:absolute; color: red";

  let coords = getCoords(elem);

  message.style.left = coords.left + "px";
  message.style.top = coords.bottom + "px";

  message.innerHTML = html;

  return message;
}

總結(jié)

頁(yè)面上的任何點(diǎn)都有坐標(biāo):

  1. 相對(duì)于窗口的坐標(biāo) —— ?elem.getBoundingClientRect()?。
  2. 相對(duì)于文檔的坐標(biāo) —— ?elem.getBoundingClientRect()? 加上當(dāng)前頁(yè)面滾動(dòng)。

窗口坐標(biāo)非常適合和 position:fixed 一起使用,文檔坐標(biāo)非常適合和 position:absolute 一起使用。

這兩個(gè)坐標(biāo)系統(tǒng)各有利弊。有時(shí)我們需要其中一個(gè)或另一個(gè),就像 CSS position 的 absolute 和 fixed 一樣。

任務(wù)


查找區(qū)域的窗口坐標(biāo)

重要程度: 5

在下面的 iframe 中,你可以看到一個(gè)帶有綠色區(qū)域(field)的文檔。

使用 JavaScript 查找?guī)Ъ^指向的角的窗口坐標(biāo)。

你的代碼應(yīng)該使用 DOM 來(lái)獲取以下窗口坐標(biāo):

  1. 左上的外角(這很簡(jiǎn)單)。
  2. 右下的外角(也挺簡(jiǎn)單)。
  3. 左上的內(nèi)角(這有點(diǎn)難)。
  4. 右下的內(nèi)角(有幾種方式,選擇其中一種)。

你計(jì)算得到的坐標(biāo),應(yīng)該與點(diǎn)擊鼠標(biāo)返回的坐標(biāo)相同。

P.S. 如果元素具有其他大?。╯ize)和邊框(border),且未綁定任何固定的值,你寫的代碼也應(yīng)該起作用。

打開一個(gè)任務(wù)沙箱。


解決方案

  • 外角
  • 外角就是我們從 elem.getBoundingClientRect() 獲取的。

    answer1 為左上角的坐標(biāo),answer2 為右下角的坐標(biāo):

    let coords = elem.getBoundingClientRect();
    
    let answer1 = [coords.left, coords.top];
    let answer2 = [coords.right, coords.bottom];
  • 左上的內(nèi)角坐標(biāo)
  • 內(nèi)角與外角主要的不同在于邊框的寬度。一種獲取距離的可靠的方法是 clientLeft/clientTop

    let answer3 = [coords.left + field.clientLeft, coords.top + field.clientTop];
  • 右下的內(nèi)角坐標(biāo)
  • 在我們的例子中,我們需要把外部坐標(biāo)減去邊框(border)大小。

    我們可以使用 CSS 的方式:

    let answer4 = [
      coords.right - parseInt(getComputedStyle(field).borderRightWidth),
      coords.bottom - parseInt(getComputedStyle(field).borderBottomWidth)
    ];

    另一種方式是把 clientWidth/clientHeight 和左上角的坐標(biāo)相加。這個(gè)方式相較于上一個(gè)或許更好:

    let answer4 = [
      coords.left + elem.clientLeft + elem.clientWidth,
      coords.top + elem.clientTop + elem.clientHeight
    ];

使用沙箱打開解決方案。


在元素旁顯示一個(gè) note

重要程度: 5

創(chuàng)建一個(gè)函數(shù) positionAt(anchor, position, elem) 來(lái)定位 elem,具體取決于 anchor 元素附近的 position

position 必須具有下列三個(gè)字符串中的一個(gè):

  • ?"top"? — 將 ?elem? 定位在 ?anchor? 上方
  • ?"right"? — 將 ?elem? 定位在 ?anchor? 右側(cè)
  • ?"bottom"? — 將 ?elem? 定位在 ?anchor? 下方

position 被用在函數(shù) showNote(anchor, position, html) 內(nèi),該函數(shù)使用給定的 html 創(chuàng)建一個(gè) “note” 元素,并將其顯示在 anchor 附近的 position 處。

這是一個(gè)演示示例:


打開一個(gè)任務(wù)沙箱。


解決方案

在這個(gè)任務(wù)中,我們只需要準(zhǔn)確地計(jì)算坐標(biāo)即可。具體細(xì)節(jié),請(qǐng)參見代碼。

請(qǐng)注意:元素必須在文檔中才能讀取 offsetHeight 和其它屬性。 隱藏的(display:none)或者不在文檔中的元素沒(méi)有大小。

使用沙箱打開解決方案。


在元素旁(absolute)顯示一個(gè) note

重要程度: 5

修改 上一個(gè)任務(wù) 的解決方案,讓 note 元素使用 position:absolute 來(lái)替代 position:fixed。

這可以防止頁(yè)面滾動(dòng)時(shí)元素的“失控”。

以上一個(gè)任務(wù)的解決方案為基礎(chǔ)。為了測(cè)試頁(yè)面滾動(dòng),請(qǐng)?zhí)砑訕邮?nbsp;<body style="height: 2000px">


解決方案

解決方案實(shí)際上很簡(jiǎn)單:

  • 在 ?.note? 的 CSS 中,使用 ?position:absolute? 代替 ?position:fixed?。
  • 使用在 坐標(biāo) 一章中所講的函數(shù) getCoords() 來(lái)獲取相對(duì)于文檔的坐標(biāo)。

使用沙箱打開解決方案。


把 note 放在元素內(nèi)部(absolute)

重要程度: 5

擴(kuò)展上一個(gè)任務(wù) 在元素旁(absolute)顯示一個(gè) note:教函數(shù) positionAt(anchor, position, elem) 把 elem 插入到 anchor 內(nèi)部。

position 的新值:

  • ?top-out?,?right-out?,?bottom-out? — 和之前一樣工作,它們把 ?elem? 插入到 ?anchor? 的上方/右側(cè)/下方。
  • ?top-in?,?right-in?,?bottom-in? — 把 ?elem? 插入到 ?anchor? 內(nèi)部:將其粘貼到上/右/下邊緣。

例如:

// 在 blockquote 上方顯示 note
positionAt(blockquote, "top-out", note);

// 在 blockquote 內(nèi)部的上邊緣顯示 note
positionAt(blockquote, "top-in", note);

結(jié)果:


可以以上一個(gè)任務(wù) 在元素旁(absolute)顯示一個(gè) note 的解決方案為基礎(chǔ)。


解決方案

使用沙箱打開解決方案。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)