30daysJS-08 - Hold Shift to Check Multiple Checkboxes
唉这个可太巧妙了,开启了新世界大门。
我自己先写了遍,我写得很想当然,一开始甚至想过clientY这种,后来想的是数组:
- 需要修改的li在一个数组内;
- 需要确定起点和终点,再遍历数组使起点和终点内的checkbox被选中。
问题就是如何确定,我的方法是:
- 新建函数A,空数组B,变量start和end:每次点击都触发A,A确定点击li在数组中的index,push到B内,最后为start赋值B[0],end赋值B[B.length - 1 ],按照start为开头,end为末尾splice总数组,得到新数组B2;
- 新建函数C,C内有一个if,if是否按着shift 和点击的e.target.checked为true,如果是的话就遍历B2,使B2内所有li的checked为true。
对于反着选也能全选的问题我是使end和start掉个个来解决的,虽然写出来最后也能运行吧,但真的完全暴露出我没用脑子,很线性的思考。
我的回答
const inbox = document.querySelector(".inbox") as HTMLElement;
const items = Array.from(document.querySelectorAll("input"));
let start: number;
let end: number;
let isKeyHolding = false;
inbox.addEventListener("click", select);
window.addEventListener("keydown", shift);
window.addEventListener("keyup", () => {
isKeyHolding = false;
arr = [];
});
function shift(e: KeyboardEvent) {
if (e.key === "Shift" && e.repeat) {
isKeyHolding = true;
}
}
let arr: number[] = [];
function select(e: MouseEvent) {
if (!isKeyHolding) return;
const item = e.target as HTMLInputElement;
if (item.tagName == "INPUT" && item.checked) {
let index = items.indexOf(item);
arr.push(index);
}
checked();
}
function checked() {
let index = arr.length;
start = arr[0]!;
end = arr[index - 1]!;
if (end !== undefined) {
if (end < start) {
let i = end;
end = start;
start = i;
}
let arrNew = items.slice(start, end);
arrNew.forEach((item) => {
item.checked = true;
});
}
}
答案
答案将事件绑定在了每个li上,只设定了两个新变量lastChecked和inBetween,lastChecked是为了确定终点,inBetween是守关变量。
实际上人只做了两件事:
- 按住shift健;
- 点击开始和结束的li。
这个题目的解题关键确实是起点和终点,但我们需要告诉程序的就是这个是不是起点或终点,程序按顺序遍历数组,不需要知道起点和终点具体在数组内的排序,只需要知道这个节点在不在起点和终点内。
每次点击都会进行判断:
checkboxes.forEach(checkbox => {
if (checkbox === this || checkbox === lastChecked) {
inBetween = !inBetween;
console.log('Starting to check them in between!');
}
if (inBetween) {
checkbox.checked = true;
}
});
所以每次区间确定也只是两次点击事件:
第一次点击事件时可以为lastChecked传值,将现在this赋值于它,第二次点击时我们已经有了一个点,现在就要确定第二次的点是起点还是终点了——起点还是终点是相对的,因为dom只会按顺序遍历,所以如果在第一次点击的A点上方点击B,则B成为起点,A成为终点,如果在A下方点击,则A成为起点,B成为终点,这是遍历过程的结果——但它们都只是确定线段区间的两点而已,同样是边界点。
inBetween就是这个区间的开关,inBetween的判定逻辑是这样的:
- 点击的
this,和遍历到的checkbox指向的节点对象是同一个, - 点击的
this,和lastChecked(上一次点击存储的this)指向的节点对象是同一个
那么就会进行inBetween的赋值,根据inBetween的结果,判定checked的值:
- 如果此
this通过判定,那么将值为false的inBetween进行重新赋值,inBetween = !inBetween,新的inBetween = true - 接下来所有遍历到的
this都没有通过判定,因为inBetween = true,所以被遍历到的checked = true,也就变成了被选中状态; - 接下来又发生了一次点击事件,新的
this通过判定,那么将值为true的inBetween进行重新赋值,inBetween = !inBetween,新的inBetween = false,后面被遍历到的元素便没有办法通过if (inBetween)判定,让checkbox.checked = true。
如此,其实在程序运行中就确定了一个动态的区间,并不是由人类手动规定好区间的,大家负责的内容不一样。
临摹自deepwiki
修改
但老师的答案也有缺陷,如果先按shift,再点击,会全选所有的todo,问题出现在第一次点击初始化的时候,可以先进行一个短路测试,如果lastChecked为假,则为lastChecked赋值为this,然后返回,结束初始化,不进行遍历。
if(!lastChecked){
lastChecked = this
return
}
老师的代码:
const checkboxes = document.querySelectorAll('.inbox input[type="checkbox"]');
let lastChecked;
console.log(lastChecked);
function handleCheck(e) {
// Check if they had the shift key down
// AND check that they are checking it
if (e.shiftKey && this.checked) {
// go ahead and do what we please
// loop over every single checkbox
let inBetween = false;
checkboxes.forEach(checkbox => {
console.log(checkbox);
if (checkbox === this || checkbox === lastChecked) {
inBetween = !inBetween;
console.log('Starting to check them in between!');
}
if (inBetween) {
checkbox.checked = true;
}
});
}
lastChecked = this;
}
checkboxes.forEach(checkbox => checkbox.addEventListener('click', handleCheck));