witch@&*weaves

30daysJS-08 - Hold Shift to Check Multiple Checkboxes

唉这个可太巧妙了,开启了新世界大门。

我自己先写了遍,我写得很想当然,一开始甚至想过clientY这种,后来想的是数组:

  1. 需要修改的li在一个数组内;
  2. 需要确定起点和终点,再遍历数组使起点和终点内的checkbox被选中。

问题就是如何确定,我的方法是:

  1. 新建函数A,空数组B,变量start和end:每次点击都触发A,A确定点击li在数组中的index,push到B内,最后为start赋值B[0],end赋值B[B.length - 1 ],按照start为开头,end为末尾splice总数组,得到新数组B2;
  2. 新建函数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上,只设定了两个新变量lastCheckedinBetweenlastChecked是为了确定终点,inBetween是守关变量。

实际上人只做了两件事:

  1. 按住shift健;
  2. 点击开始和结束的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的判定逻辑是这样的:

那么就会进行inBetween的赋值,根据inBetween的结果,判定checked的值:

  1. 如果此this通过判定,那么将值为falseinBetween 进行重新赋值, inBetween = !inBetween,新的 inBetween = true
  2. 接下来所有遍历到的this都没有通过判定,因为inBetween = true,所以被遍历到的checked = true,也就变成了被选中状态;
  3. 接下来又发生了一次点击事件,新的this通过判定,那么将值为true inBetween 进行重新赋值, inBetween = !inBetween,新的 inBetween = false,后面被遍历到的元素便没有办法通过if (inBetween)判定,让checkbox.checked = true

如此,其实在程序运行中就确定了一个动态的区间,并不是由人类手动规定好区间的,大家负责的内容不一样。

yM8RGr.png

临摹自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));

#code