2022 年 4 月 25 日

鍵盤:keydown 和 keyup

在我們進入鍵盤之前,請注意在現代裝置上還有其他「輸入內容」的方法。例如,人們使用語音辨識(特別是在行動裝置上)或使用滑鼠複製/貼上。

因此,如果我們想要追蹤輸入到 <input> 欄位的任何內容,那麼鍵盤事件是不夠的。還有一個名為 input 的事件,用於追蹤 <input> 欄位的變更,不論透過任何方式。對於這類任務來說,這可能是一個更好的選擇。我們將在 事件:change、input、cut、copy、paste 章節中介紹它。

當我們想要處理鍵盤動作(虛擬鍵盤也算)時,應該使用鍵盤事件。例如,對箭頭鍵 向上向下 或熱鍵(包括按鍵組合)做出反應。

測試台

若要更深入瞭解鍵盤事件,您可以使用以下測試台。

在文字欄位中嘗試不同的按鍵組合。

結果
script.js
style.css
index.html
kinput.onkeydown = kinput.onkeyup = kinput.onkeypress = handle;

let lastTime = Date.now();

function handle(e) {
  if (form.elements[e.type + 'Ignore'].checked) return;

  area.scrollTop = 1e6;

  let text = e.type +
    ' key=' + e.key +
    ' code=' + e.code +
    (e.shiftKey ? ' shiftKey' : '') +
    (e.ctrlKey ? ' ctrlKey' : '') +
    (e.altKey ? ' altKey' : '') +
    (e.metaKey ? ' metaKey' : '') +
    (e.repeat ? ' (repeat)' : '') +
    "\n";

  if (area.value && Date.now() - lastTime > 250) {
    area.value += new Array(81).join('-') + '\n';
  }
  lastTime = Date.now();

  area.value += text;

  if (form.elements[e.type + 'Stop'].checked) {
    e.preventDefault();
  }
}
#kinput {
  font-size: 150%;
  box-sizing: border-box;
  width: 95%;
}

#area {
  width: 95%;
  box-sizing: border-box;
  height: 250px;
  border: 1px solid black;
  display: block;
}

form label {
  display: inline;
  white-space: nowrap;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <form id="form" onsubmit="return false">

    Prevent default for:
    <label>
      <input type="checkbox" name="keydownStop" value="1"> keydown</label>&nbsp;&nbsp;&nbsp;
    <label>
      <input type="checkbox" name="keyupStop" value="1"> keyup</label>

    <p>
      Ignore:
      <label>
        <input type="checkbox" name="keydownIgnore" value="1"> keydown</label>&nbsp;&nbsp;&nbsp;
      <label>
        <input type="checkbox" name="keyupIgnore" value="1"> keyup</label>
    </p>

    <p>Focus on the input field and press a key.</p>

    <input type="text" placeholder="Press keys here" id="kinput">

    <textarea id="area" readonly></textarea>
    <input type="button" value="Clear" onclick="area.value = ''" />
  </form>
  <script src="script.js"></script>


</body>
</html>

KeyDown 和 KeyUp

當按鍵按下時,會發生 keydown 事件,然後在按鍵放開時,會發生 keyup 事件。

event.code 和 event.key

事件物件的 key 屬性可取得字元,而事件物件的 code 屬性可取得「實體按鍵代碼」。

例如,相同的按鍵 Z 可以搭配或不搭配 Shift 按下。這會產生兩個不同的字元:小寫 z 和大寫 Z

event.key 正好是字元,而且會不同。但 event.code 是相同的。

按鍵 event.key event.code
Z z(小寫) KeyZ
Shift+Z Z(大寫) KeyZ

如果使用者使用不同的語言,切換到另一種語言會產生與 "Z" 完全不同的字元。這將成為 event.key 的值,而 event.code 始終相同:"KeyZ"

「KeyZ」和其他按鍵代碼

每個按鍵都有代碼,取決於其在鍵盤上的位置。按鍵代碼在 UI 事件代碼規範 中說明。

例如

  • 字母鍵的代碼為 "Key<letter>""KeyA""KeyB" 等。
  • 數字鍵的代碼為:"Digit<number>""Digit0""Digit1" 等。
  • 特殊鍵的代碼為其名稱:"Enter""Backspace""Tab" 等。

有數種廣泛使用的鍵盤配置,而規範為每種配置提供按鍵代碼。

閱讀 規範的字母數字部分 以取得更多代碼,或只要在 測試台 中按下按鍵即可。

大小寫很重要:"KeyZ",而不是 "keyZ"

這似乎很明顯,但人們還是會犯錯。

請避免錯字:是 KeyZ,而不是 keyZ。像 event.code=="keyZ" 這樣的檢查無法運作:"Key" 的第一個字母必須是大寫。

如果一個按鍵沒有提供任何字元,該怎麼辦?例如,ShiftF1 或其他。對於這些按鍵,event.key 大約等於 event.code

按鍵 event.key event.code
F1 F1 F1
Backspace Backspace Backspace
Shift Shift ShiftRightShiftLeft

請注意,event.code 精確指定按下的按鍵。例如,大多數鍵盤都有兩個 Shift 按鍵:一個在左側,一個在右側。event.code 告訴我們確切按下哪一個,而 event.key 負責按鍵的「意義」:它是什麼(一個「Shift」)。

假設我們想要處理一個熱鍵:Ctrl+Z(或 Mac 的 Cmd+Z)。大多數文字編輯器都會在上面掛接「復原」動作。我們可以在 keydown 上設定一個監聽器,並檢查按下哪個按鍵。

這裡有一個兩難:在這樣的監聽器中,我們應該檢查 event.keyevent.code 的值嗎?

一方面,event.key 的值是一個字元,它會根據語言而改變。如果訪客在作業系統中有多種語言並在它們之間切換,則相同的按鍵會提供不同的字元。因此檢查 event.code 是有道理的,它始終相同。

像這樣

document.addEventListener('keydown', function(event) {
  if (event.code == 'KeyZ' && (event.ctrlKey || event.metaKey)) {
    alert('Undo!')
  }
});

另一方面,event.code 有問題。對於不同的鍵盤配置,相同的按鍵可能具有不同的字元。

例如,以下是美國配置(「QWERTY」)和其下方的德語配置(「QWERTZ」)(來自維基百科)

對於相同的按鍵,美國配置有「Z」,而德語配置有「Y」(字母互換)。

從字面上看,當德語配置的人按下 Y 時,event.code 等於 KeyZ

如果我們在程式碼中檢查 event.code == 'KeyZ',那麼對於德語配置的人,當他們按下 Y 時,這樣的測試將會通過。

這聽起來真的很奇怪,但事實就是如此。規格 明確提到了這種行為。

因此,event.code 對於意外的配置可能與錯誤的字元相符。不同配置中的相同字母可能會對應到不同的實體按鍵,從而導致不同的代碼。幸運的是,這只發生在幾個代碼上,例如 keyAkeyQkeyZ(如我們所見),並且不會發生在 Shift 等特殊按鍵上。您可以在 規格 中找到清單。

要可靠地追蹤與配置相關的字元,event.key 可能會是更好的方式。

另一方面,event.code 的好處是始終保持相同,與實體按鍵位置相關。因此,即使切換語言,依賴它的熱鍵也能正常運作。

我們要處理與配置相關的按鍵嗎?那麼 event.key 是可行的方式。

或者我們希望熱鍵在切換語言後也能運作?那麼 event.code 可能會更好。

自動重複

如果按住一個按鍵夠久,它會開始「自動重複」:keydown 會不斷觸發,然後在放開時,我們最後會收到 keyup。因此,出現許多 keydown 和一個 keyup 是很正常的。

對於由自動重複觸發的事件,事件物件會將 event.repeat 屬性設定為 true

預設動作

預設動作會有所不同,因為鍵盤可以啟動許多可能的動作。

例如

  • 螢幕上會出現一個字元(最明顯的結果)。
  • 刪除一個字元(Delete 鍵)。
  • 捲動頁面(PageDown 鍵)。
  • 瀏覽器開啟「儲存頁面」對話方塊(Ctrl+S
  • …等等。

除了作業系統的特殊鍵之外,在 keydown 上防止預設動作可以取消大部分的動作。例如,在 Windows 上,Alt+F4 會關閉目前的瀏覽器視窗。而且沒有辦法透過在 JavaScript 中防止預設動作來阻止它。

例如,下面的 <input> 預期會輸入電話號碼,因此它不接受數字以外的按鍵,例如 +()-

<script>
function checkPhoneKey(key) {
  return (key >= '0' && key <= '9') || ['+','(',')','-'].includes(key);
}
</script>
<input onkeydown="return checkPhoneKey(event.key)" placeholder="Phone, please" type="tel">

這裡的 onkeydown 處理常式使用 checkPhoneKey 來檢查按下的按鍵。如果有效(從 0..9+-() 之一),則傳回 true,否則傳回 false

正如我們所知,事件處理常式傳回的 false 值會防止預設動作,例如上面使用 DOM 屬性或屬性指定的方式,因此對於未通過測試的按鍵,<input> 中不會出現任何內容。(傳回的 true 值不會影響任何內容,只有傳回 false 才會有影響)

請注意,特殊鍵,例如 BackspaceLeftRight,在輸入時不會運作。這是嚴格篩選器 checkPhoneKey 的副作用。這些按鍵會讓它傳回 false

讓我們放寬篩選器,允許使用箭頭鍵 LeftRightDeleteBackspace

<script>
function checkPhoneKey(key) {
  return (key >= '0' && key <= '9') ||
    ['+','(',')','-','ArrowLeft','ArrowRight','Delete','Backspace'].includes(key);
}
</script>
<input onkeydown="return checkPhoneKey(event.key)" placeholder="Phone, please" type="tel">

現在箭頭鍵和刪除鍵可以正常運作了。

即使我們有按鍵篩選器,仍然可以使用滑鼠和右鍵 + 貼上來輸入任何內容。行動裝置提供其他輸入值的方式。因此,篩選器並非 100% 可靠。

替代方法是追蹤 oninput 事件,它會在任何修改之後觸發。我們可以在這裡檢查新的 input.value,並在它無效時修改或突顯 <input>。或者,我們可以同時使用這兩個事件處理常式。

舊版

過去有一個 keypress 事件,以及事件物件的 keyCodecharCodewhich 屬性。

在使用這些屬性時,有許多瀏覽器不相容的情況,因此規格的開發人員別無他法,只能將它們全部棄用,並建立新的現代事件(如本章前面所述)。舊程式碼仍然有效,因為瀏覽器仍支援它們,但完全沒有必要再使用它們了。

行動裝置鍵盤

在使用虛擬/行動裝置鍵盤(正式稱為輸入法編輯器 (IME))時,W3C 標準指出 KeyboardEvent 的 e.keyCode 應該是 229,而 e.key 應該是 "Unidentified"

雖然其中一些鍵盤在按下特定按鍵(例如箭頭或退格鍵)時,仍可能對 e.keye.codee.keyCode… 使用正確的值,但並無保證,因此您的鍵盤邏輯可能無法在行動裝置上正常運作。

摘要

按下按鍵總是會產生鍵盤事件,無論是符號鍵或特殊鍵,例如 ShiftCtrl 等。唯一的例外是 Fn 鍵,有時會出現在筆電鍵盤上。它沒有鍵盤事件,因為它通常是在比作業系統更低層級實作的。

鍵盤事件

  • keydown – 按下按鍵時(如果按鍵按住很長一段時間,會自動重複),
  • keyup – 放開按鍵時。

主要的鍵盤事件屬性

  • code – 「按鍵代碼」("KeyA""ArrowLeft" 等),特定於鍵盤上按鍵的物理位置。
  • key – 字元("A""a" 等),對於非字元按鍵,例如 Esc,通常與 code 的值相同。

過去,鍵盤事件有時用於追蹤表單欄位中的使用者輸入。這並不可靠,因為輸入可能來自各種來源。我們有 inputchange 事件來處理任何輸入(稍後在本章 事件:change、input、cut、copy、paste 中介紹)。它們會在任何類型的輸入後觸發,包括複製貼上或語音辨識。

當我們真的需要鍵盤時,應該使用鍵盤事件。例如,對熱鍵或特殊鍵做出反應。

任務

重要性:5

建立一個函式 `runOnKeys(func, code1, code2, ... code_n)`,當同時按下代碼為 `code1`、`code2`、…、`code_n` 的按鍵時,執行 `func`。

例如,以下程式碼會在同時按下 `「Q」` 和 `「W」` 時顯示 `alert`(在任何語言中,不論是否開啟 CapsLock)

runOnKeys(
  () => alert("Hello!"),
  "KeyQ",
  "KeyW"
);

在新視窗中示範

我們應該使用兩個處理常式:`document.onkeydown` 和 `document.onkeyup`。

讓我們建立一個集合 `pressed = new Set()` 來儲存目前按下的按鍵。

第一個處理常式會將按鍵加入集合,而第二個處理常式會將按鍵從集合中移除。每次在 `keydown` 時,我們會檢查是否已按下足夠的按鍵,如果已按下,則執行函式。

在沙盒中開啟解決方案。

教學地圖

留言

留言前請先閱讀…
  • 如果您有改善建議,請 提交 GitHub 問題 或發起拉取請求,而不是留言。
  • 如果您無法理解文章中的某個部分,請詳細說明。
  • 若要插入幾行程式碼,請使用 `<code>` 標籤;若要插入多行程式碼,請將其包覆在 `<pre>` 標籤中;若要插入超過 10 行的程式碼,請使用沙盒(plnkrjsbincodepen…)