iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 10
0
Modern Web

一起挑戰 JavaScript 30 吧!系列 第 10

JS30 Day 10 - Hold Shift and Check Checkboxes

成品連結:Hold Shift and Check Checkboxes程式碼

今天要做的是選單多重選取!這功能就像是當你在資料夾選取一個項目,然後按著 shift 並選取另一個檔案,會發現兩個檔案之間的所有檔案都變成選取狀態了。今天我們要做的就是這個功能

事前規劃

關於今天的主題我的想法是:

  1. 建立一個變數來記錄最後打勾的 checkbox
  2. 建立新的陣列來儲存上次打勾與目前打勾的之間的全部元素
  3. 最後將新陣列中的全部元素都打勾

當然還需要考慮到使用者是否有按下 shift (有按下 shift 的狀態才會選取區間內的 checkbox),但話不多說我們就開始吧!

開始寫程式碼

:::warning
:zap: 程式碼由於拆解看起來可能有點亂,完整版請見 GitHub 程式碼連結
:::
從 CSS 可以看到當 input[type="checkbox"] 被勾選後,同一層的 p 會被畫線,所以們我只需要單純處理 input[type="checkbox"] 的狀態就好而不需要去管其它元素

input:checked + p {
    background: #F9F9F9;
    text-decoration: line-through;
}

首先先選取全部的 input[type="checkbox"]

const checkboxes = document.querySelectorAll('input');

偵測 shift 是否按下

接著建立一個變數來辨識使用者是否有按下 shift,預設 false,接著使用監聽事件來監測狀態:如果事件是 keydown 且按下 shift,則變數變為 true;反之當 keyup 時且無按下 shift,則變數變為 false

let holdingShift = false;

window.addEventListener('keydown', function(e) {
    if (e.keyCode === 16) {  // keyCode 16 代表 shift
        holdingShift = true;
    }
});

window.addEventListener('keyup', function(e) {
    holdingShift = false;
});

追蹤最後打勾的元素

接著建立新變數來儲存最後打勾的 checkbox

let lastChecked;

使用 let 而不是 const 是因為之後值會變動

然後為每個 checkbox 設定監聽事件,並把被點取的 checkbox 存入 lastChecked 變數

checkBoxes.forEach((checkbox, index) => {
    checkbox.addEventListener('click', function(e) {
        // 執行打勾功能
    
        lastChecked = checkBoxes[index];
    }
});

建立新陣列儲存要打勾的元素

這裡開始要做一些判斷了,當 lastChecked 已被打勾且使用者按下 shift 時,將 lastChecked 與目前點選項目之間的元素存入新的陣列

checkBoxes.forEach((checkbox, index) => {
    checkbox.addEventListener('click', function(e) {
        // 執行打勾功能
        if (lastChecked.checked && holdingShift) {
            let newList;
        }
    
        lastChecked = checkBoxes[index];
    }
});

此時會發現當按下 checkbox 時遇到錯誤:Uncaught TypeError: Cannot read property 'checked' of undefined,這是因為一開始我們並沒有賦予 lastChecked 值,故目前還是 undefined,所以需要另做處理

checkBoxes.forEach((checkbox, index) => {
    checkbox.addEventListener('click', function(e) {
        // 當 lastChecked 是 undefined 時,lastChecked = 被點選的元素
        if (lastChecked === undefined) {
            lastChecked = checkbox;
        }
        
        // 執行打勾功能
        if (lastChecked.checked && holdingShift) {
            let newList;
        }
        
        lastChecked = checkBoxes[index];
    }
});

建立要加入 newList 的元素

至此我們已經有了當前點擊元素 & 前一次點擊的元素(lastChecked),可以來補充 newList 的內容了!
我的想法是比較當前點擊元素 & 前一次點擊的元素(lastChecked)在 checkBoxes 陣列中的位置,並把在中間的元素加入新陣列。有什麼方法可以截取陣列的部分?

沒錯,就是使用 slice()!但 slice() 並不是 nodeList 可用的 method,所以需要先將原本的 checkBoxes 轉成陣列

為了要判斷順序,需要在全域再宣告新的變數 lastIndex 來紀錄上次點選元素的 index,並且在監聽事件中與 lastChecked 一同更新

const checkBoxes = Array.from(document.querySelectorAll('.inbox input[type="checkbox"]'));
let holdingShift = false;
let lastIndex;
let lastChecked;

checkBoxes.forEach((checkbox, index) => {
    checkbox.addEventListener('click', function(e) {
        // 當 lastChecked 是 undefined 時,lastChecked = 被點選的元素
        if (lastChecked === undefined) {
            lastChecked = checkbox;
        }
        
        // 執行打勾功能
        if (lastChecked.checked && holdingShift) {
            let newList;
        }
        
        lastIndex = index;
        lastChecked = checkBoxes[index];
    }
});

有時候可能會先點後方項目再點擊前面項目(例如點擊第 5 項再點擊第 2 項),所以要將各種情況做不同處理

if (index < lastIndex) {
    newList = checkBoxes.slice(index, lastIndex);
} else if (lastIndex < index) {
    newList = checkBoxes.slice(lastIndex, index);
} else return;  // 當 index = lastIndex 時直接 return ,不然會做出長度為 0 的陣列

接著就把 newList 內全部的項目打勾即可!

newList.forEach(checkbox => {
    checkbox.checked = true;
    console.log(checkbox);
});

完成!

小補充

這裡順便補充一下右鍵的觸發事件;左鍵我們都知道是 click,而右鍵是 contextmenu 喔!

Reference


上一篇
JS30 Day 9 - Dev Tools Domination
下一篇
JS30 Day 11 - Custom Video Player
系列文
一起挑戰 JavaScript 30 吧!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言