JavaScript 與 jQuery

第 6 章    事件

∗ 當使用者瀏覽網頁時

▸ 瀏覽器會登記各種型態的事件 (Event) ,例如:網頁載入、點擊按鈕或連結、滑鼠縈繞、鍵盤輸入、視窗縮放等

▸ JavaScript 或 jQuery 程式可以偵測這些事件並進一步處理,例如修改 DOM 內容

(1) 事件型態

∗ 使用者介面事件 (UI event):使用者與瀏覽器介面互動時產生

▸ 載入 (load):網頁完成載入

▸ 卸載 (unload):網頁完成卸載 (因為新網頁載入)

▸ 錯誤 (error):瀏覽器遭遇 JavaScript 程式錯誤,或資產 (Asset) 不存在

▸ 縮放 (resize):瀏覽器視窗縮放

▸ 捲動 (scroll):使用者向上或向下捲動

∗ 鍵盤事件 (Keyboard event):使用者與鍵盤互動時產生

▸ 鍵按下 (keydown):使用者按下某個按鍵 (未鬆開按鍵前持續發生)

▸ 鍵彈上 (keyup):使用者鬆開按鍵

▸ 按鍵 (keypress):使用者輸入某個字元 (未鬆開按鍵前持續送出字元)

∗ 滑鼠事件 (Mouse event):使用者與滑鼠、觸控板、或觸控螢幕互動時產生

▸ 點擊 (click):使用者在同一個 HTML 元素上按下並鬆開滑鼠鍵

▸ 雙擊 (dblclick):使用者在同一個 HTML 元素上按下並鬆開滑鼠鍵兩次

▸ 鍵按下 (mousedown):使用者在 HTML 元素上按下滑鼠鍵

▸ 鍵彈上 (mouseup):使用者在 HTML 元素上鬆開滑鼠鍵

▸ 滑鼠移動 (mousemove):使用者移動滑鼠 (手持裝置之觸控螢幕不適用)

▸ 滑鼠移至 (mouseover):使用者將滑鼠移到某個 HTML 元素之上 (手持裝置之觸控螢幕不適用)

▸ 滑鼠移開 (mouseoout):使用者將滑鼠移開某個 HTML 元素 (手持裝置之觸控螢幕不適用)

∗ 聚焦事件 (Focus event):當 HTML 元素獲得聚焦或失焦時產生

▸ 聚焦 (focus/focusin):聚焦某元素

▸ 失焦 (blur/focusout):元素失焦

∗ 表單事件 (Form event):使用者與表單互動時產生

▸ 變動 (change):在選單 (Selection)、複選框 (Checkbox)、或單選紐 (Radio button) 裡的值變動

▸ 送出 (submit):使用者送出表單

(2) 事件之處理

∗ 事件之處理程序:

▸ 什麼元素該回應事件,亦即應將事件綁定 (Bind) 在哪個元素

▸ 什麼事件 (Event) 會觸發

▸ 事件觸發時,所需執行程式的內容 (Code)

∗ 將事件綁定元素 (Bind an event to an element)

▸ 語法:

$(<selector>).on(<event>, function() {
  ... 
});
$(<selector>).on(...):將事件綁定在所選擇的元素上 (<selector> 為選擇器)
<event>:事件名稱
function() { ... }:當事件發生所需執行的函式

▸ 例如:

$('#price').on('change', function() {
  ...
});
# 當 id="price" 元素 (選單、單選框、複選框) 的內容改變時,觸發此事件

∗ 範例:使用者註冊時,如果輸入帳號長度過短會顯示錯誤訊息

▸ 選擇元素:使用者帳號的文字輸入欄位

▸ 指定事件:當游標移出欄位則觸發事件 (blurfocusout)

▸ 呼叫程式:事件發生則執行函式來檢查帳號長度,如果過短則顯示訊息要求使用者輸入較長的帳號,當使用者更正帳號符合要求時,清除錯誤訊息

▸ 練習

✶ 下載 assets6.zip, 解壓縮後將 ch6.css 置於 ch6/css/ 目錄中, 其餘影像均置於 ch6/img/ 目錄中
blurEvent.html :
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="css/ch6.css">
</head>
<body>
<div id="page">
  <h1>買雜貨</h1>
  <h2>新帳號</h2>
  <form method="post" action="http://www.example.org/register/">
    <label>帳號:<input type="text" name="username"></label>
    <div id="feedback"></div>
    <label>密碼:<input type="password" name="password"></label>
    <input type="submit" value="註冊">
  </form>
</div>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="js/blurEvent.js"></script>
</body>
</html>
blurEvent.js :
$(document).ready(function() {

  var $feedback = $('#feedback');
  
  $('input[name=username]').on('blur', function() {
    var $this = $(this);
    if ($this.val().length < 5) {
      $feedback.html('帳號至少 5 個字元');
      $this.focus();
    }
    else {
      $feedback.html('');
    }
  });

});
var $feedback = $('#feedback'):選擇 id="feedback" 的元素, 並指派給 $feedback 變數
$('input[name=username]').on('blur', function() { ... }): 將事件綁定在帳號輸入的元素,如果游標移出元素 (觸發 blur 事件), 呼叫匿名函式來處理
var $this = $(this):this 是觸發事件的元素, $(this) 產生一個 jQuery 物件,此處將其指派給 $this 變數, 便於後續使用
if ($this.val().length < 5):如果本元素 (<input type=text ...>) 的值的長度小於 5
$feedback.html('...') :設定 $feedback 元素的內容
$this.focus():重新將游標設定在本元素,以便使用者重新輸入
$feedback.html(''):清除 $feedback 內容

(3) 事件流

∗ 事件流 (Event flow)

▸ HTML 元素以巢狀方式包覆其他元素,發生在某元素的事件會傳到其他元素,稱為事件流

▸ 事件傳遞方向有兩種模式:事件冒泡 (Event bubbling) 及事件捕獲 (Event capturing)

✶ 事件冒泡:事件流由發生的元素向外流動,因此最先偵測到事件的是發生事件的元素
→ 點擊 <a> 元素,事件流的順序為 <a>, <li>, <ul>, <body>, <html>, document, window
✶ 事件捕獲:事件流由最外層向內流動,因此最先偵測到事件的是最外層元素
→ 點擊 <a> 元素,事件流的順序為 window, document, <html>, <body>, <ul>, <li>, <a>
ch5

▸ jQuery 事件監聽器使用 Bubbling 模式

(4) 事件代表與事件物件

∗ 事件代表 (Event delegation)

▸ 為大量的元素建立事件監聽器會使瀏覽器效能大幅降低,例如 :

<ul>
  <li id="click1">點擊1</li>
  <li id="click2">點擊2</li>
  <li id="click3">點擊3</li>
  <li id="click4">點擊4</li>
</ul>
✶ 如果要為每個 <li> 連結建立事件監聽器,就要建立 4 個 :
for (var i=1; i<5; i++) {
  $('#click'+i).on('blur', function() {
    ...
  } 
}

▸ 動態產生的元素也需額外撰寫事件綁定程式,例如:如果動態加入 <li id="click5">點擊 5</li> 元素,就需要另外再加上一個事件監聽器

▸ 解決方案 : 將一個事件監聽器建立在事件代表元素上,例如 <ul>, <body>, <html>, document, 或 window ,有以下優點 :

✶ 只有一個事件監聽器,不佔太多記憶體,維持瀏覽器效能
✶ 程式精簡許多,也不需為每個事件元素定義不同 id
✶ 如果動態增加或刪除項目,也不需要修改或增加事件監聽器
✶ 例如:將所有事件綁定在頂級元素 document 上,再加上選擇器選擇目標元素 :
$(document).on(<event>, <selector>, function() {
  ...
});
✶ 以先前的 <li> 點擊範例即可如下修改 :
$(document).on('blur', 'li', function() {
  ...
});
→ 如此,僅需要建立一個監聽器,而且動態加入或刪除 <li> 元素也不會有問題

∗ 事件物件 (Event object)

▸ 當事件發生後,會產生一個事件物件,內含該事件的相關資訊以及發生所在元素

▸ jQuery 會自動將事件元素傳給事件處理器,如下 :

$(document).on(<event>, <selector>, function(e) {
  console.log(e);
});
✶ 將事件物件之參數名稱設定為 e (event 之意) ,由 jQuery 自動傳入

(5) 改變瀏覽器預設行為

∗ 取消瀏覽器預設行為

▸ 瀏覽器對於某些事件會有預設行為 (Default behavior),例如點選連結或送出表單會轉到另一個頁面

▸ 事件處理完畢後,如果不需要瀏覽器進一步處理,可在程式開頭使用 preventDefault() 函式來取消預設行為 (例如點選連結後還是停留在同一頁面):

$(document).on(<event>, <selector>, function(e) {
  e.preventDefault();
  ...
});

∗ 停止事件傳播

▸ 事件處理完畢後,如果需要停止事件傳播,可在程式開頭使用 stopPropagation() 函式來停止事件傳播:

$(document).on(<event>, <selector>, function(e) {
  e.stopPropagation();
  ...
});

∗ 取消瀏覽器預設行為,並且停止事件傳播

▸ 事件處理完畢後,在程式結尾使用 return false 來取消預設行為,同時停止事件傳播

$(document).on(<event>, <selector>, function(e) {
  ...
  return false;
});

∗ 練習:購買雜貨清單

▸ 當使用者點擊某項雜貨時,表示已購買,應將該項目刪除

eventDelegation.html :
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="css/ch6.css">
</head>
<body>

<div id="page">
  <h1 id="header">清單</h1>
  <h2>買雜貨</h2>
  <ul id="shoppingList">
    <li class="hot"><a href="/itemDone/1/"><em>新鮮</em>無花果</a></li>
    <li class="hot"><a href="/itemDone/2/">松子</a></li>
    <li class="hot"><a href="/itemDone/3/">蜂蜜</a></li>
    <li class="hot"><a href="/itemDone/4/">香醋</a></li>
  </ul>
</div>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="js/eventDelegation.js"></script>
</body>
</html>
eventDelegation.js
$(document).ready(function() {

  $(document).on('click', 'a', function() {
    $(this).parent().remove();
    return false;
  });

});
✶ 將 click 事件綁定 $(document) 物件成為事件代表,並選擇 <a> 元素為實際觸發事件的元素
✶ 當事件觸發,從本元素 $(this) 上移一層到 <li> 元素,然後將其刪除
✶ 最後利用 return false 取消預設行為及停止事件傳播

∗ 作業 1

(6) 使用者介面事件

∗ 使用者介面事件 (User interface event, UI event)

▸ 使用者與瀏覽器 (不是 HTML) 互動所產生的事件

▸ 事件處理器應綁定 window 物件

事件 觸發
load HTML 子元素完成載入,例如 : image, script, iframe, window
unload 頁面卸載 (載入另一頁面)
error HTML 元素載入錯誤
resize 瀏覽器視窗縮放
scroll 捲軸上下捲動

* 練習 :

windowLoad.html (類似 blurEvent.html):

...
<script src="js/windowLoad.js"></script>
...

js/windowLoad.js :

$(window).on('load', function() {
  $('input[name=username]').focus();
});

▸ 當頁面子元素載入完畢,聚焦在帳號輸入欄位

▸ HTML 有 autofocus 屬性,效能較好

<input type="text" name="username" autofocus>

(7) 聚焦/失焦事件

∗ 聚焦/失焦事件 (Focus/blur event)

事件 觸發
focus 元素聚焦
blur 元素失焦

▸ 如果 focus 的目的僅為改變元素樣式,使用 CSS 的 :focus 較好

* 練習 :

focusBlur.html (類似 blurEvent.js):

...
<script src="js/focusBlur.js"></script>
...

js/focusBlur.js :

$(document).ready(function() {

  var minLength = 5;
  var $feedback = $('#feedback');
  
  $(document).on('focus', 'input[name=username]', function() {
    var usernameLen = $(this).val().length;
    
    if (usernameLen == 0) {
      $feedback.attr('class', 'tip');
      $feedback.html('帳號長度至少 ' + minLength + ' 個字元');
    }
    else if  (usernameLen < minLength){
      $feedback.attr('class', 'warning');
      $feedback.html('帳號長度不夠');    
    }
  });
  
  $(document).on('blur', 'input[name=username]', function() {
    var $this = $(this);
    if ($this.val().length < minLength) {
      $this.focus();
    }
    else {
      $feedback.html('');
    }
  });

});

▸ 綁定聚焦與失焦兩個事件

▸ 輸入欄聚焦時:如果使用者尚未輸入任何資料,設定 tip 類別並顯示提示訊息; 如果帳號長度不夠,設定 warning 類別並警告訊息

▸ 輸入欄失焦時:如果帳號長度不夠,重新聚焦輸入欄,否則清除訊息

(8) 滑鼠事件

* 滑鼠事件 (Mouse event)

事件 觸發 觸控螢幕
click 點擊滑鼠左鍵 點擊螢幕
dblclick 點擊滑鼠左鍵兩次 雙擊螢幕
mousedown 按下滑鼠鍵 可使用 touchstart 事件
mouseup 鬆開滑鼠鍵 可使用 touchend 事件
mouseover 滑鼠指標指向某元素或其子節點
mouseout 滑鼠指標移開某元素或其子節點
mousemove 滑鼠指標在同一元素移動 (持續觸發)

▸ 如果 mouseovermouseout 的目的僅為改變元素樣式, 使用 CSS 的 :hover 較好

* 練習:

clickEvent.html (類似 blurEvent.html):

...
<script src="js/clickEvent.js"></script>
...

js/clickEvent.js :

$(document).ready(function() {

  var msg = '<div id="note">';
  msg += '<div class="header"><a id="close" href="#">關閉 X</a></div>';
  msg += '<div><h2>系統維護</h2>本站伺服器在凌晨 3 點至 4 點之間將升級,';
  msg += '在這段時間有些服務將停止。</div>';
  msg += '</div>'

  $('#page').append(msg);
  
  $(document).on('click', '#close', function() {
    $('#note').remove();
    return false;
  });
    
});

▸ 動態產生一個 HTML 元素 msg 並附加到頁面

▸ 綁定點擊事件,觸發事件後,將該訊息刪除

∗ 事件發生位置

▸ 事件在實體螢幕的位置 (藍框):screenX, screenY

▸ 事件在整個文件的位置 (綠框):pageX, pageY

▸ 事件在可見文件的位置 (紅框):clientX, clientY

ch5

▸ 練習:

positionEvent.html:(類似 eventDelegation.html):
...
</head>
<body>

<div id="stats">
  screenX: <input id="sx" type="text"> 
  screenY: <input id="sy" type="text"><span class="divider">|</span> 
  pageX: <input id="px" type="text">
  pageY: <input id="py" type="text"><span class="divider">|</span>
  clientX: <input id="cx" type="text"> 
  clientY: <input id="cy" type="text">
</div>

<div id="page">
  ...

<script src="js/positionEvent.js"></script>
...
js/positionEvent.js :
$(document).ready(function() {

  var $sx = $('#sx');
  var $sy = $('#sy');
  var $px = $('#px');
  var $py = $('#py');
  var $cx = $('#cx');
  var $cy = $('#cy');

  $(document).on('mousemove', 'body', function(e) {
    $sx.val(e.screenX);
    $sy.val(e.screenY);
    $px.val(e.pageX);
    $py.val(e.pageY);
    $cx.val(e.clientX);
    $cy.val(e.clientY);
  });

});

(9) 鍵盤事件

∗ 鍵盤事件

事件 觸發
keydown 使用者按下鍵盤按鍵 (持續觸發)
keypress 使用者按鍵輸入一個字元 (持續觸發,Shift, Esc, Delete 等非列印按鍵不會觸發)
keyup 使用者鬆開鍵盤按鍵

keydownkeypress 事件發生在字元輸出之前, keyup 發生在字元出現之後

▸ 字元資訊儲存在事件物件的 which 屬性中

* 練習 :

keyupEvent.html:

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="css/ch6.css">
</head>
<body>

<div id="page">
  <h1>買雜貨</h1>
  <h2>我的側寫</h2>
  <textarea id="message"></textarea>
  <div id="charactersLeft"><span>180</span> 個字元</div>
  <div id="lastKey"></div>
</div>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="js/keyupEvent.js"></script>
</body>
</html>

js/keyupEvent.js :

$(document).ready(function() {

  $(document).on('keyup', '#message', function(e) {
    $('#charactersLeft > span').html(180 - $(this).val().length);
    $('#lastKey').html('最後按鍵 ASCII 碼:' + e.which);
  });

});

keyupkeydown 均無法判斷字母大小寫, 只有 keypress 可以

(10) 表單事件

∗ 表單事件

事件 觸發
submit 表單送出
change 表單下拉選單、複選框、單選紐等輸入元素的值 (value) 變動
input 表單輸入欄的值 (value) 變動

▸ 表單事件處理常包含表單驗證 (Form validation),確保使用者所輸入的資料正確

* 練習:當使用者

▸ 與下拉選單互動時,顯示相關訊息

▸ 送出表單時,檢查是否勾選同意條款

formEvent.html :

<!doctype html>
<html>
<head>
<title>ddd</title>
<meta charset="utf-8">
<link rel="stylesheet" href="css/ch6.css">
</head>
<body>

<div id="page">
  <h1>List King</h1>
  <form id="formSignup" method="post" action="/accounts/register/">
    <h2>Membership</h2>

    <label for="package" class="selectbox">選擇方案:</label>
    <select id="package">
      <option value="annual">1 年 ($50)</option>
      <option value="monthly">1 個月 ($5)</option>
    </select>
    <div id="packageHint" class="tip"></div>

    <input type="checkbox" id="terms">
    <label for="terms" class="checkbox">同意條款</label>
    <div id="termsHint" class="warning"></div>

    <input type="submit" value="下一步">

  </form>
</div>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="js/formEvent.js"></script>
</body>
</html>

js/formEvent.js :

$(document).ready(function() {

  $(document).on('change', '#package', function() {
    var $packageHint = $('#packageHint');
    
    if ($(this).val() == 'monthly') {
      $packageHint.html('如果選擇 1 年方案可以節省 $1,000 元!');
    }
    else {
      $packageHint.html('聰明的選擇!');
    }
  });
  
  $(document).on('submit', '#formSignup', function() {
    if (!$('#terms').is(':checked')) {
      $('#packageHint').html('請勾選以同意條款。');
      return false;
    }
  });
  
});

▸ 綁定 changesubmit 兩個事件:

change:依選擇方案提示訊息
submit:檢查是否勾選「同意條款」

∗ 作業 2

上一章       下一章