向前邁進

Sérgio Gomes

過去,在網路上指向某個項目很簡單。你有滑鼠,你移動滑鼠,有時按下按鈕,就這樣而已。所有非滑鼠的裝置都會模擬成滑鼠,開發人員也能確切掌握要依賴的裝置。

不過,簡單不一定是好事。隨著時間推移,我們發現並非所有裝置都需要 (或假裝成) 滑鼠,因此使用壓力感應和傾斜感應筆,可讓你盡情發揮創意;你也可以使用手指,只要有裝置和手就行;不過,為何不使用多個手指呢?

我們已經有了觸控事件,可以協助我們解決這個問題,但這些事件是專門用於觸控的完全獨立 API,因此如果您想同時支援滑鼠和觸控,就必須編寫兩個獨立的事件模型。Chrome 55 提供新標準,可整合這兩種模型:指標事件。

單一事件模型

指標事件會將瀏覽器的指標輸入模式統一,將觸控、觸控筆和滑鼠整合為一組事件。例如:

document.addEventListener('pointermove',
    ev => console.log('The pointer moved.'));
foo.addEventListener('pointerover',
    ev => console.log('The pointer is now over foo.'));

以下是所有可用事件的清單,如果您熟悉滑鼠事件,應該會覺得相當熟悉:

pointerover 游標已進入元素的定界框。對於支援懸停的裝置,這會立即發生;對於不支援的裝置,則會在 pointerdown 事件發生前發生。
pointerenter pointerover 類似,但不會向上傳遞,並以不同方式處理子項。規格詳細資料
pointerdown 指標已進入有效按鈕狀態,視輸入裝置的語意而定,可能是按下按鈕或建立了接觸。
pointermove 游標已變更位置。
pointerup 指標已離開啟用按鈕狀態。
pointercancel 發生了某些事件,因此指標不太可能再發出任何事件。也就是說,您應取消任何進行中的動作,並返回中性輸入狀態。
pointerout 游標已離開元素或畫面的邊界框。同樣地,如果裝置不支援懸停,pointerup 後也會發生這種情況。
pointerleave pointerout 類似,但不會向上傳遞,並以不同方式處理子項。規格詳細資料
gotpointercapture 元素已收到指標擷取
lostpointercapture 已擷取的指標已釋放。

不同的輸入類型

一般來說,指標事件可讓您以輸入無關的方式編寫程式碼,而不需要為不同的輸入裝置註冊個別的事件處理常式。當然,您仍需留意輸入類型之間的差異,例如是否適用懸停概念。如果您想區分不同的輸入裝置類型 (例如為不同輸入提供不同的程式碼/功能),您可以使用 PointerEvent 介面的 pointerType 屬性,在同一個事件處理常式中執行這項操作。舉例來說,如果您正在為側邊導覽匣編寫程式碼,可以在 pointermove 事件中使用下列邏輯:

switch(ev.pointerType) {
    case 'mouse':
    // Do nothing.
    break;
    case 'touch':
    // Allow drag gesture.
    break;
    case 'pen':
    // Also allow drag gesture.
    break;
    default:
    // Getting an empty string means the browser doesn't know
    // what device type it is. Let's assume mouse and do nothing.
    break;
}

預設動作

在支援觸控的瀏覽器中,使用特定手勢可捲動、縮放或重新整理網頁。在觸控事件的情況下,您仍會在這些預設動作發生時收到事件,例如,在使用者捲動時,系統仍會觸發 touchmove

使用指標事件時,只要觸發捲動或縮放等預設動作,就會收到 pointercancel 事件,讓您知道瀏覽器已控制指標。例如:

document.addEventListener('pointercancel',
    ev => console.log('Go home, the browser is in charge now.'));

內建速度:與觸控事件相比,這個模型預設可提供更佳效能,您需要使用被動事件監聽器才能達到相同的回應速度。

您可以使用 touch-action CSS 屬性,停止瀏覽器的控制權。如果在元素上將其設為 none,系統就會停用所有在該元素上啟動的瀏覽器定義動作。不過,還有許多其他值可用於更精細的控制,例如 pan-x,可讓瀏覽器對 x 軸上的移動做出反應,但不會對 y 軸做出反應。Chrome 55 支援下列值:

auto 預設值;瀏覽器可以執行任何預設動作。
none 瀏覽器不得執行任何預設動作。
pan-x 瀏覽器只能執行水平捲動的預設動作。
pan-y 瀏覽器只能執行垂直捲動的預設動作。
pan-left 瀏覽器只能執行水平捲動的預設動作,且只能將頁面平移至左側。
pan-right 瀏覽器只能執行水平捲動的預設動作,且只能將頁面平移至右側。
pan-up 瀏覽器只能執行垂直捲動的預設動作,且只能將頁面平移至上方。
pan-down 瀏覽器只能執行垂直捲動的預設動作,且只能向下平移網頁。
manipulation 瀏覽器只能執行捲動和縮放動作。

游標擷取

您是否曾經花費一小時來偵錯錯誤的 mouseup 事件,最後才發現問題是使用者在點擊目標之外放開按鈕?沒有?好吧,那麼可能只有我有這個問題。

不過,目前還沒有很好的方法可以解決這個問題。當然可以,您可以在文件上設定 mouseup 處理常式,並在應用程式上儲存部分狀態,以便追蹤相關內容。不過,如果您正在建構網頁元件,並希望將所有內容保持良好且隔離的狀態,這並非最簡潔的解決方案。

使用指標事件可提供更優質的解決方案:您可以擷取指標,確保能取得 pointerup 事件 (或其他難以捉摸的事件)。

const foo = document.querySelector('#foo');
foo.addEventListener('pointerdown', ev => {
    console.log('Button down, capturing!');
    // Every pointer has an ID, which you can read from the event.
    foo.setPointerCapture(ev.pointerId);
});

foo.addEventListener('pointerup', 
    ev => console.log('Button up. Every time!'));

瀏覽器支援

在撰寫本文時,Internet Explorer 11、Microsoft Edge、Chrome 和 Opera 支援指標事件,Firefox 則部分支援。如要查看最新清單,請前往 caniuse.com

您可以使用指標事件 polyfill 來填補空白。或者,您也可以在執行階段檢查瀏覽器支援情形,方法如下:

if (window.PointerEvent) {
    // Yay, we can use pointer events!
} else {
    // Back to mouse and touch events, I guess.
}

指標事件是漸進式強化的絕佳候選項目:只要修改初始化方法來進行上述檢查、在 if 區塊中新增指標事件處理常式,並將滑鼠/觸控事件處理常式移至 else 區塊即可。

歡迎試用,並與我們分享你的想法!