2022 年 6 月 19 日

聚焦:focus/blur

當使用者點選元素或使用鍵盤上的 Tab 鍵時,元素會接收焦點。另外,還有一個 autofocus HTML 屬性,可在頁面載入時預設將焦點放在元素上,以及其他取得焦點的方法。

聚焦在元素上通常表示:「準備在此處接受資料」,因此這是我們可以執行程式碼以初始化所需功能的時機。

失去焦點(「模糊」)的時刻可能更重要。這是當使用者點選其他地方或按下 Tab 以前往下一個表單欄位時,或者還有其他方法。

失去焦點通常表示:「資料已輸入」,因此我們可以執行程式碼來檢查它,甚至將它儲存到伺服器等等。

在使用焦點事件時,有一些重要的特性。我們將盡力在後續說明它們。

焦點/模糊事件

focus 事件在聚焦時呼叫,而 blur 則在元素失去焦點時呼叫。

讓我們使用它們來驗證輸入欄位。

在以下範例中

  • blur 處理常式檢查欄位中是否已輸入電子郵件,如果沒有,則顯示錯誤。
  • focus 處理常式隱藏錯誤訊息(在 blur 中,它會再次檢查)
<style>
  .invalid { border-color: red; }
  #error { color: red }
</style>

Your email please: <input type="email" id="input">

<div id="error"></div>

<script>
input.onblur = function() {
  if (!input.value.includes('@')) { // not email
    input.classList.add('invalid');
    error.innerHTML = 'Please enter a correct email.'
  }
};

input.onfocus = function() {
  if (this.classList.contains('invalid')) {
    // remove the "error" indication, because the user wants to re-enter something
    this.classList.remove('invalid');
    error.innerHTML = "";
  }
};
</script>

現代 HTML 允許我們使用輸入屬性進行許多驗證:requiredpattern 等。有時它們正是我們需要的。當我們需要更多彈性時,可以使用 JavaScript。如果值正確,我們也可以自動將變更的值傳送至伺服器。

焦點/模糊方法

方法 elem.focus()elem.blur() 設定/取消元素的焦點。

例如,讓我們在值無效時,讓訪客無法離開輸入

<style>
  .error {
    background: red;
  }
</style>

Your email please: <input type="email" id="input">
<input type="text" style="width:220px" placeholder="make email invalid and try to focus here">

<script>
  input.onblur = function() {
    if (!this.value.includes('@')) { // not email
      // show the error
      this.classList.add("error");
      // ...and put the focus back
      input.focus();
    } else {
      this.classList.remove("error");
    }
  };
</script>

它在所有瀏覽器中都能運作,唯 Firefox 除外(錯誤)。

如果我們在輸入中輸入一些內容,然後嘗試使用 Tab 或從 <input> 點擊離開,則 onblur 會將焦點返回。

請注意,我們無法在 onblur 中呼叫 event.preventDefault() 來「防止失去焦點」,因為 onblur 在元素失去焦點之後才會運作。

然而,在實務上,在實作此類功能之前,應該仔細思考,因為我們通常應該向使用者顯示錯誤,但不應阻止他們繼續填寫我們的表單。他們可能想要先填寫其他欄位。

JavaScript 引發的焦點遺失

焦點遺失可能發生於許多原因。

其中之一是當訪客點選其他地方時。但 JavaScript 本身也可能造成它,例如

  • alert 會將焦點移至自身,因此它會導致元素失去焦點(blur 事件),而當 alert 被關閉時,焦點會回來(focus 事件)。
  • 如果從 DOM 中移除元素,也會導致焦點遺失。如果稍後重新插入它,則焦點不會回來。

這些功能有時會導致 focus/blur 處理常式行為不當,在不需要時觸發。

最佳做法是在使用這些事件時要小心。如果我們想要追蹤使用者引發的焦點遺失,則我們應避免自己造成它。

允許聚焦於任何元素:tabindex

預設情況下,許多元素不支援聚焦。

清單在瀏覽器之間略有不同,但有一件事始終正確:focus/blur 支援保證適用於訪客可以互動的元素:<button><input><select><a> 等。

另一方面,用於格式化某些內容的元素,例如 <div><span><table>,預設無法取得焦點。方法 elem.focus() 對這些元素無效,而且絕不會觸發 focus/blur 事件。

可以使用 HTML 屬性 tabindex 來變更這個設定。

任何元素只要有 tabindex,就會變成可取得焦點。當使用 Tab(或類似功能)在這些元素之間切換時,屬性的值就是元素的順序號碼。

也就是說:如果我們有兩個元素,第一個的 tabindex="1",第二個的 tabindex="2",那麼在第一個元素時按下 Tab,焦點就會移到第二個元素。

切換順序為:具有 tabindex1 以上的元素會優先(依據 tabindex 順序),然後才是沒有 tabindex 的元素(例如一般的 <input>)。

沒有符合 tabindex 的元素會依據文件來源順序(預設順序)切換。

有兩個特殊值

  • tabindex="0" 會將元素置於沒有 tabindex 的元素之間。也就是說,當我們切換元素時,具有 tabindex=0 的元素會在具有 tabindex ≥ 1 的元素之後。

    通常用於讓元素可取得焦點,但維持預設的切換順序。讓元素成為與 <input> 平行的表單一部分。

  • tabindex="-1" 只允許使用程式碼將焦點設定在元素上。Tab 鍵會忽略這些元素,但方法 elem.focus() 有效。

例如,這裡有一個清單。按一下第一個項目,然後按下 Tab

Click the first item and press Tab. Keep track of the order. Please note that many subsequent Tabs can move the focus out of the iframe in the example.
<ul>
  <li tabindex="1">One</li>
  <li tabindex="0">Zero</li>
  <li tabindex="2">Two</li>
  <li tabindex="-1">Minus one</li>
</ul>

<style>
  li { cursor: pointer; }
  :focus { outline: 1px dashed green; }
</style>

順序如下:1 - 2 - 0。一般來說,<li> 不支援取得焦點,但 tabindex 可以完全啟用這個功能,以及事件和使用 :focus 的樣式。

屬性 elem.tabIndex 也有效

我們可以使用 elem.tabIndex 屬性,透過 JavaScript 新增 tabindex。效果相同。

委派:focusin/focusout

事件 focusblur 沒有冒泡效果。

例如,我們無法在 <form> 上放置 onfocus 以突顯它,如下所示

<!-- on focusing in the form -- add the class -->
<form onfocus="this.className='focused'">
  <input type="text" name="name" value="Name">
  <input type="text" name="surname" value="Surname">
</form>

<style> .focused { outline: 1px solid red; } </style>

上面的範例無法運作,因為當使用者將焦點放在 <input> 上時,focus 事件只會觸發在該輸入上。它不會冒泡。因此 form.onfocus 永遠不會觸發。

有兩種解決方案。

首先,有一個有趣的歷史功能:focus/blur 沒有冒泡,但會在擷取階段向下傳播。

這將會運作

<form id="form">
  <input type="text" name="name" value="Name">
  <input type="text" name="surname" value="Surname">
</form>

<style> .focused { outline: 1px solid red; } </style>

<script>
  // put the handler on capturing phase (last argument true)
  form.addEventListener("focus", () => form.classList.add('focused'), true);
  form.addEventListener("blur", () => form.classList.remove('focused'), true);
</script>

其次,有 focusinfocusout 事件,與 focus/blur 完全相同,但它們會冒泡。

請注意,它們必須使用 elem.addEventListener 指派,而不是 on<event>

以下是另一個可運作的變體

<form id="form">
  <input type="text" name="name" value="Name">
  <input type="text" name="surname" value="Surname">
</form>

<style> .focused { outline: 1px solid red; } </style>

<script>
  form.addEventListener("focusin", () => form.classList.add('focused'));
  form.addEventListener("focusout", () => form.classList.remove('focused'));
</script>

摘要

事件 focusblur 會在元素取得/失去焦點時觸發。

它們的特殊功能是

  • 它們不會冒泡。可以改用擷取狀態或 focusin/focusout
  • 大多數元素預設不支援焦點。使用 tabindex 使任何東西都可以取得焦點。

目前取得焦點的元素可用於 document.activeElement

任務

重要性:5

建立一個在被按一下時會變成 <textarea><div>

該 textarea 允許編輯 <div> 中的 HTML。

當使用者按下 Enter 或失去焦點時,<textarea> 會變回 <div>,而其內容會變成 <div> 中的 HTML。

在新視窗中示範

為任務開啟沙盒。

重要性:5

讓表格儲存格在按一下時可編輯。

  • 按一下時,儲存格應變成「可編輯」(textarea 出現在裡面),我們可以變更 HTML。不應調整大小,所有幾何形狀應保持不變。
  • 儲存格下方會出現確定和取消按鈕,以完成/取消編輯。
  • 一次只能編輯一個儲存格。當 <td> 處於「編輯模式」時,按一下其他儲存格會被忽略。
  • 表格可能有多個儲存格。使用事件委派。

示範

為任務開啟沙盒。

  1. 按一下時,使用相同大小和無邊框的 <textarea> 取代儲存格的 innerHTML。可以使用 JavaScript 或 CSS 來設定正確的大小。
  2. textarea.value 設定為 td.innerHTML
  3. 將焦點放在 textarea 上。
  4. 在儲存格下方顯示確定/取消按鈕,處理按一下事件。

在沙盒中開啟解決方案。

重要性:4

將焦點放在滑鼠上。然後使用箭頭鍵移動它

在新視窗中示範

附註:不要在 #mouse 元素以外的任何地方放置事件處理常式。

附註的附註:不要修改 HTML/CSS,方法應為通用,且適用於任何元素。

為任務開啟沙盒。

我們可以使用 mouse.onclick 來處理按一下事件,並使用 position:fixed 使滑鼠「可移動」,然後使用 mouse.onkeydown 來處理箭頭鍵。

唯一的缺點是 keydown 僅會觸發在有焦點的元素上。因此我們需要將 tabindex 加入元素中。由於我們被禁止變更 HTML,因此我們可以使用 mouse.tabIndex 屬性。

附註:我們也可以用 mouse.onfocus 取代 mouse.onclick

在沙盒中開啟解決方案。

教學課程地圖

留言

留言前請閱讀…
  • 如果您有改進建議,請提交 GitHub 問題或提交拉取請求,而不是留言。
  • 如果您無法理解文章中的內容,請詳細說明。
  • 要插入少數幾個字的程式碼,請使用 <code> 標籤;要插入多行程式碼,請將其包在 <pre> 標籤中;要插入 10 行以上的程式碼,請使用沙盒 (plnkrjsbincodepen…)