最终效果

大概效果就是这样子,点击某个范围内的元素有会有一个状态(这里是一个列表出现的效果),然后在点击非这个区域范围的话就会变成另外一个状态(列表消失)
GIF.gif

🎈 曲折的过程

开始

将上面的效果再次解析就是:在 A 元素上,点击它本身是一种状态(比如更换背景色为蓝色),点击外部的又是另外一个状态(背景色变成红色)
做法就可以在 A 元素直接绑定 click 事件,还要阻止冒泡(这里为什么需要阻止冒泡呢?
GIF.gif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
#targetDiv {
width: 200px;
height: 200px;
}
</style>
</head>

<body>
<div id="targetDiv"></div>
<script>
let targetDiv = document.querySelector('#targetDiv');

targetDiv.addEventListener('click', (event) => {
event.stopPropagation();
targetDiv.style.backgroundColor = 'blue';
});
document.addEventListener('click', () => {
targetDiv.style.backgroundColor = 'red';
});
</script>
</body>
</html>

事件冒泡:事件从触发事件的目标元素开始,逐级向上级冒泡到 DOM 树的根节点
在这里就会从#test -> body -> html -> document
这样子又会导致触发下面那个事件方法将他背景设置为红色,因此会看不到变成蓝色

取消冒泡呢

在浏览器鼠标点击事件中有三个事件:mousedownmouseupclick
mousedown:按下鼠标按键(左、右键均可)不需要松开
mouseup:松开鼠标按键(左、右键均可)
click:按下并松开鼠标左键
在触发顺序上:

  • 若在同一个元素上按下并松开鼠标左键,会依次触发mousedownmouseupclick,前一个事件执行完毕才会执行下一个事件
  • 若在同一个元素上按下并松开鼠标右键,会依次触发mousedownmouseup,前一个事件执行完毕才会执行下一个事件,不会触发 click 事件

因此可以同使用mousedown来替代click事件,但此时还需要知道的就是:我点击的是否在某个元素的元素里面(也可以理解为是否为目标元素的子孙元素)呢?
elementA.contains(elementB):如果 elementB 是不是 elementA 的后代元素,包括本身,返回true or false
所以可以将上面的代码改造成下面这样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
#targetDiv {
width: 200px;
height: 200px;
}
</style>
</head>

<body>
<div id="targetDiv"></div>
<script>
const targetDiv = document.getElementById('targetDiv');
console.log(targetDiv);

// 使用 mousedown 事件替代 click 事件
document.addEventListener('mousedown', (event) => {
if (targetDiv.contains(event.target)) {
// 点击发生在目标div内部
targetDiv.style.backgroundColor = 'blue';
} else {
// 点击发生在目标div外部
targetDiv.style.backgroundColor = 'red';
}
});
</script>
</body>
</html>

优化

前面提到的都是使用 js 来进行操作,但有没有办法不使用 js 而是用 css 来实现,以此能提高性能呢?
:focus-within:CSS 伪类表示当元素或其任意后代元素被聚焦时,将匹配该元素。换言之,它表示 :focus 伪类匹配到该元素自身或它的后代时,该伪类生效。
由于使用该伪类的时候需要聚焦,默认 div 是不会接收焦点的,加上 tabindex="0"就可以接收焦点了。
效果如下:
GIF.gif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
#container {
padding: 20px;
}

#targetDiv {
width: 200px;
height: 100px;
background-color: red;
transition: background-color 0.3s;
}

#targetDiv:focus-within {
background-color: blue;
}
</style>
</head>

<body>
<div id="container">
<div
id="targetDiv"
tabindex="0">
点击我或我的内部
<button>内部按钮</button>
</div>
</div>
</body>
</html>

div -> input

一般聚焦的话就是 input 输入框(语义不符),因此把上面提及的 div 更改为 input,再次实现类似的效果。
在这种情况下开始感觉到有股神秘力量把我拉到了之前画学校官网的时候,下拉列表的实现
当时是用的:hover也实现了类似的效果。
GIF.gif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
#container {
padding: 20px;
}

#targetInput {
width: 200px;
height: 100px;
background-color: red;
transition: background-color 0.3s;
border: none;
padding: 10px;
font-size: 16px;
color: white;
}

#targetInput:hover,
#targetInput:focus {
background-color: blue;
outline: none;
}

#innerButton {
position: relative;
top: 50px;
left: 10px;
}
</style>
</head>

<body>
<div id="container">
<input
id="targetInput"
type="text"
placeholder="点击我或悬停在我上面" />
</div>
</body>
</html>

然后神奇的是,手机端没有 鼠标 这个概念,但还是可以进行变色,原因就是 移动端的hover表现还是跟focus一样

加入子列表

加入子列表之后看起来就是原生的 select组件 效果了
GIF.gif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
#container {
padding: 20px;
}

#targetInput {
height: 10px;
background-color: red;
transition: background-color 0.3s;
border: none;
padding: 10px;
font-size: 16px;
color: white;
}

#targetInput:focus {
background-color: blue;
outline: none;
}

.item {
display: none;
margin-top: 10px;
}

#targetInput:focus ~ .item {
display: block;
}

#innerButton {
position: relative;
top: 50px;
left: 10px;
}
</style>
</head>

<body>
<div id="container">
<input
id="targetInput"
type="text"
placeholder="点击我" />
<div class="item">
<div>子列表1</div>
<div>子列表2</div>
<div>子列表3</div>
</div>
</div>
</body>
</html>

但此时又会出现问题,就是在移动端的时候会聚焦输入框,触发 软键盘

GIF.gif
解决办法:加上 readonly 属性或者设置 disabled 属性
readonly:通过将输入框设置为只读,移动设备上的软键盘将不会弹出。 用户仍然可以通过其他方式复制、粘贴或选择文本。
disabled:通过将输入框禁用,移动设备上的软键盘将不会弹出,并且用户无法对其进行任何操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
#container {
padding: 20px;
}

#targetInput {
height: 10px;
background-color: red;
transition: background-color 0.3s;
border: none;
padding: 10px;
font-size: 16px;
color: white;
}

#targetInput:focus {
background-color: blue;
outline: none;
}

.item {
display: none;
margin-top: 10px;
}

#targetInput:focus ~ .item {
display: block;
}

#innerButton {
position: relative;
top: 50px;
left: 10px;
}
</style>
</head>

<body>
<div id="container">
<input
read-only
id="targetInput"
type="text"
placeholder="点击我或悬停在我上面" />
<div class="item">
<div>子列表1</div>
<div>子列表2</div>
<div>子列表3</div>
</div>
</div>
</body>
</html>

总结

  1. 在检测点击的是否是某个元素的时候可以使用 e.target 来进行获取,在此基础上也可以使用事件冒泡来做一个事件委托,达到性能提高的目的。
  2. div 本身语义就不应该有聚焦,所以换成 input 更佳
  3. 在考虑页面效果的时候不止要 web 端,还得考虑其他可能的平台:比如移动端、客户端等的效果展示