普通文組 2.5

自主練習「純手工pagination」技術記錄

總結

練習為 pagination 加上 JavaScript,使其可與使用者互動。

  • 使用到的技術
    • DOM Traversing: parent, siblings
  • 新學習到的概念
    • pointer-events: none
    • parseInt()Number()的差異

成品

See the Pen pagination practice by Charlie (@Charlie7779) on CodePen.

環境

Google Chrome: 90.0.4430.72 (Official Build) (64-bit)
Bootstrap: 5.0.0-beta3
os: Windows_NT 10.0.18363 win32 x64

流程

  1. 使用Bootstrap: Pagination的 HTML 原始碼,並移除不需要的部分
  • aria-label在練習中用不到,移除
  • <li>使用 JavaScript 產生,全部移除;加上備註,提示內容會由 JavaScript 處理
  • 整理完畢後,HTML 部分的原始碼如下
<nav>
<ul class="pagination justify-content-center" id="pagination">
<!-- generated by JavaScript -->
</ul>
</nav>
  1. 粗略分解任務:「使用者與 pagination 互動時,會發生什麼事情?」
  • 點選 previous 或 next 來前進、後退
  • 直接點選頁數來進行換頁
  • 抵達頁數的「頭」或「尾」後,繼續點選 previous 或 next 不會有任何事情發生
  • 換頁後,畫面須呈現相對應的內容
  1. 將以上任務流程精緻化
  • 「點選 previous 或 next 來前進、後退」與「直接點選頁數來進行換頁」
    • 需監聽滑鼠點擊事件
    • 需判定滑鼠點擊的目標是 previous、next 或頁數
    • 要判定使用者點擊 pagination 後會在「哪一頁」
  • previous 或 next 與頁數頭尾互動事件:
    • 使用者抵達首尾頁後,點擊 previous 或 next 不可繼續前進或後退
    • 使用者不在首尾頁時,previous 或 next 應恢復功能
  • 換頁後,畫面須呈現相對應的內容
    • 要判定使用者點擊 pagination 後會在「哪一頁」
  1. 使用程式碼滿足以上列出的需求

JavaScript 原始碼

全域變數

  • pageCount:儲存總頁數的變數;除了直接賦值外,也可承接其他 function 回傳的值。比如在 ALPHA Camp 的練習情境中,pageCount可承接「電影總數(80 筆)除以每一頁的電影數量(12 筆)後無條件進位」此情境的總頁數數量(pagination 應展示 7 頁)
  • initialPage:畫面載入時,預設顯示的頁面;在本練習中預設顯示第 1 頁

dynamicallyGeneratePage (page)

目的:根據傳入的總頁數(page)數量來產生 pagination;比如傳入 8,即要產生 8 頁的 pagination

  • 第 3 行:加入 Previous 按鈕。而因為起始頁是第一頁(initialPage設定為 1),故 Previous 按鈕預設是不會有任何功能的(不可繼續前進),所以在<li>加上.disabled
  • 第 4-8 行:加入頁數,頁數根據傳入dynamicallyGeneratePage()的參數決定。並將initialPage加上.active,標記其為起始頁
  • 第 9 行:加入 Next 按鈕
  • 第 10 行:將第三行到第九行的內容放進paginationdocument.querySelector('#pagination'))中
  • 第 11 行:設定displaydocument.querySelector('#display'))內容
  • 關於data-page:使用dataset為每一個<a>元素加上data-page,讀取data-page的值即可判定該<a>為 Previous、Next 或頁數按鈕

innerHTMLContent (page)

目的:根據傳入的頁數,產生對應的畫面內容

  • 第 4 行:根據傳入的page來產生相對應的頁數
  • 第 5 行:使用Lorem Picsum透過 id指定特定圖片的服務,根據傳入的page來產生圖片,達成換頁換圖的效果
  • 第 7 行:innerHTMLContent()回傳的值可直接賦予innerHTML

paginationStatusUpdate (event)

目的:根據使用者點擊的目標,更新 pagination 的狀態;並呼叫innerHTMLContent()來修改畫面內容

  • 第 2 行:const pageData = event.target.dataset.page
    • 點擊事件發生後,抓取「被點擊的目標」其data-page的值,每一個<a>元素的data-page是透過dynamicallyGeneratePage()產生的
    • 點擊到 Previous 時,pageDatap;點擊到 Next 時,pageDatan
    • 點擊到處於disabled狀態的 Previous 與 Next 時,pageDataundefined,理由如下:
      • 參考 Bootstrap 5.0.0-beta3 的原始碼可得知.page-item.disabled .page-link的設定為
      .page-item.disabled .page-link {
      color: #6c757d;
      pointer-events: none;
      background-color: #fff;
      border-color: #dee2e6;
      }
      注意這段選取器的作用對象是.page-item.disabled 下的「.page-link」,而 HTML 結構如下
      <li class="page-item disabled">
      <a class="page-link" href="#" data-page="p">Previous</a>
      </li>
      所以pointer-events: none;實際作用的對象是<a>
      • 根據MDN 的規格pointer-events設定為none時,該元素(在本練習中為<a>)無法成為滑鼠游標點擊事件的目標(原文:The element is never the target of pointer events.)
      • 所以在 Previous 與 Next 處於disabled時,<a>不會成為滑鼠游標點擊事件的目標;在這樣的情況下,游標點擊事件會觸發的目標會變成該元素的親元素(在本練習中為<li>)(原文:In these circumstances, pointer events will trigger event listeners on this parent element as appropriate on their way to/from the descendant during the event capture/bubble phases.)
      • <li>沒有設定pageData,所以當我想要取<li>pageData的值的時候,我會得到undefined
    • 結論:pageData的值有以下可能性
      • undefined:代表 Previous 與 Next 處於disabled的狀態
      • pn:當 Previous 與 Next 不處於disabled時,點擊 Previous 或 Next 會讓pageData的值為pn
      • 頁數的數字:當使用者點擊頁數時,pageData的值就是該頁數的數字,但資料型態會是String
  • 第 3 行:const activePage = document.querySelector('#pagination li.active')
    • 取「處於active狀態」的<li>
  • 第 4 行:const activePageNumber = activePage.firstElementChild.dataset.page
    • 會取得點擊事件發生時,處於active狀態的<li>其子元素<a>data-page資料
    • 舉例:若目前.active的是第 1 頁,而我點了第 4 頁,activePageNumber的值會是 1
  • 第 5-6 行:固定取得 Previous<li>與 Next<li>,因為.disabled.active是在<li>上操作的
  • 第 8 行開始的switch (pageData)
    • case undefined:代表使用者在 Previous 或 Next 處於disabled時點擊這兩個按鈕,這時不應該再繼續執行翻頁的動作,故直接return,什麼事都不發生
    • case "p"
      • 代表使用者點擊 Previous 時,此按鈕不處於disabled狀態,所以 Previous 應該帶領使用者前往上一頁
      • 第 12 行代表「點擊 Previous 時從最後一頁離開」,這時 Next 就不應繼續處於disabled狀態,故需移除.disabled
      • 第 14-16 行:代表「點擊 Previous 時正在通過第二頁」,而從第二頁往前移動後,就會位在第一頁,位在第一頁後就不可繼續往前翻頁了,所以 Previous 要加上.disabled,進入disabled狀態
      • 第 18-19 行:若點擊 Previous 時,Previous 不符合第 12-16 行列出的條件,就進行「移除目前處於active頁數的.active,並為前一頁加上.active
      • 第 20 行:因為移動到前一頁了,所以display的內容也要更新,換成前一頁(activePageNumber - 1)的內容
    • case "n":換頁的邏輯與 Previous 相反
      • 第 23 行代表「點擊 Next 時從第一頁離開」,這時 Previous 就不應繼續處於disabled狀態,故需移除.disabled
      • 第 25-27 行:代表「點擊 Next 時正在通過倒數第二頁」,而從倒數第二頁往後移動後,就會位在最末頁,位在最末頁後就不可繼續往後翻頁了,所以 Next 要加上.disabled,進入disabled狀態
      • 第 29-31 行:若點擊 Next 時,Next 不符合第 23-27 行列出的條件,就進行「移除目前處於active頁數的.active,並為下一頁加上.active
    • default:代表pageData不屬於上述任何一種狀態,亦即使用者直接點選了頁數
      • 第 34 行:移除activePageactive狀態
      • 第 35 行:為event.target(被滑鼠點選到)的parentElement(即是<li>)加上active狀態
      • 第 36 行:更新display的內容
      • 第 38-40 行:使用者點選到最末頁時,Next 要變為disabled狀態
      • 第 42-44 行:使用者點選到第一頁時,Previous 要變為disabled狀態

bonus track:parseInt()Number()的差別

  • 在過去的專案與練習中,習慣直接使用parseInt()來將字串解析(parsing)為數字,本次改為使用Number()來將字串型變(type converting)為數字
  • 雖然這兩者都可以回傳我需要的資料,但好奇這其中是否有差異,而經過搜尋後得知:
  • Number()做的事:convert
    • Annotated ECMAScript 5.1: When Number is called as a function (rather than as a constructor), it performs a type conversion.
    • MDN: Values of other types can be converted to numbers using the Number() function.
  • parseInt()做的事:parse,而非 convert
    • MDN: The Number.parseInt() method parses a string argument and returns an integer of the specified radix or base.
  • 結論:雖然都可以透過傳入String得到Number,但parseInt()Number()處理的手法並不一樣

參考文件