实现点击非目标指令

本文最后更新于:2023年11月5日 晚上

需求

满足以下一个条件即可:
在鼠标抬起和按下时的元素为非指令元素自身或子元素时就触发事件

效果

打开控制台查看

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
<script>
import clickoutside from "./f-clickoutside";

export default {
data () {
return {
clickFn: this.demoFn1
}
},
directives: {
clickoutside
},
methods: {
demoFn1 () {
console.log('点击事件1')
},
demoFn2 () {
console.log('点击事件2')
},
changeClickFn () {
this.clickFn = this.clickFn === this.demoFn1 ? this.demoFn2 : this.demoFn1
}
}
}
</script>

<template>
<div class="demo-div" v-clickoutside="clickFn">
目标元素
<button style="user-select: none" @click="changeClickFn">改变点击事件</button>
</div>
</template>

<style>
* {
margin: 0;
padding: 0;
}

.demo-div {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
font-size: 25px;
color: white;
width: 150px;
height: 150px;
background: #5e6d82;
user-select: none;
}
</style>

初次实现

vue2 版本

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
/**
* @author Xin-FAS
*/

// 为一个元素添加鼠标监听事件
const createListener = (el = {}, cb = () => {}) => {
let mousedownNode
// 记录按下时的元素给后续判断
const onMousedown = e => mousedownNode = e.target
document.addEventListener('mousedown', onMousedown)
// 在鼠标每次抬起时判断(满足鼠标抬起和按下时的元素都不为当前元素自身或子元素才调用方法)
const onMouseup = e => {
if (
e.target === el ||
mousedownNode === el ||
el.contains(e.target) ||
el.contains(mousedownNode)
) return
cb()
}
document.addEventListener('mouseup', onMouseup)
// 给元素绑定上,在指令更新时取消监听
el.mouseListener = {
onMousedown,
onMouseup
}
}
// 取消元素上的监听事件
const removeListener = el => {
const {
onMousedown,
onMouseup
} = el.mouseListener
document.removeEventListener('mouseup', onMouseup)
document.removeEventListener('mousedown', onMousedown)
}
export default {
bind (el, binding) {
createListener(el, binding.value)
},
update (el, binding) {
if (binding.value === binding.oldValue) return
// 取消现有的监听事件
removeListener(el)
// 重新监听
createListener(el, binding.value)
},
unbind (el) {
removeListener(el)
// 删除元素上的属性
delete el.mouseListener
}
}

实现过程就是在指令初始化时给元素建立监听事件,使用document.contains方法判断条件是否满足

vue3 版本

因为这还需要优化,所以不提供vue3使用

指令优化

对于element-ui中的v-clickoutside的源码分析可以先看这里:分析v-clickoutside源码

element-ui的源码分析完成发现,整体上的思路需要优化:

  1. 应该在一个监听事件中判断元素,而不是让每一个元素都建立一个监听事件
  2. 缺少对低版本浏览器的兼容考虑

最后版本

vue2 版本

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
61
62
63
/**
* @author Xin-FAS
*/

// 修改自 element-ui 源码中的 on 方法
const onListener = (function() {
if (document.addEventListener) return function(element, event, handler) {
if (element && event && handler) element.addEventListener(event, handler)
}
return function(element, event, handler) {
if (element && event && handler) element.attachEvent('on' + event, handler)
}
})()
// 存放全部元素
const nodeList = []
// 唯一指令属性名
const ctx = 'Xin-FAS:clickoutside'
// 鼠标按下时的元素
let mousedownNode
// 用于生成唯一id
let seed = 0
// 监听鼠标按下
onListener(document, 'mousedown', e => mousedownNode = e.target)
// 监听鼠标抬起
onListener(document, 'mouseup', e => nodeList.forEach(el => documentHandler(el, e.target)))
// 判断是否执行的主要逻辑
const documentHandler = (el, mouseupNode) => {
if (
!el[ctx] ||
el.contains(mouseupNode) ||
el.contains(mousedownNode)
) return
el[ctx].cb()
}
export default {
bind (el, binding) {
// 添加进监听列表,同时给元素上添加处理时需要的属性
nodeList.push(el)
el[ctx] = {
cb: binding.value,
id: seed++
}
},
update (el, binding) {
// 重置元素上的属性
el[ctx] = {
cb: binding.value,
id: seed++
}
},
unbind (el) {
// 先从监听列表中移除该元素
for (let [index, item] of nodeList.entries()) {
if (item.id === el[ctx].id) {
nodeList.splice(index, 1)
break
}
}
// 删除该元素上的属性
delete el[ctx]
}
}

vue3 版本

vue3就只是改下生命周期名称

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
61
62
/**
* @author Xin-FAS
*/

// 修改自 element-ui 源码中的 on 方法
const onListener = (function() {
if (document.addEventListener) return function(element, event, handler) {
if (element && event && handler) element.addEventListener(event, handler)
}
return function(element, event, handler) {
if (element && event && handler) element.attachEvent('on' + event, handler)
}
})()
// 存放全部元素
const nodeList = []
// 唯一指令属性名
const ctx = 'Xin-FAS:clickoutside'
// 鼠标按下时的元素
let mousedownNode
// 用于生成唯一id
let seed = 0
// 监听鼠标按下
onListener(document, 'mousedown', e => mousedownNode = e.target)
// 监听鼠标抬起
onListener(document, 'mouseup', e => nodeList.forEach(el => documentHandler(el, e.target)))
// 判断是否执行的主要逻辑
const documentHandler = (el, mouseupNode) => {
if (
!el[ctx] ||
el.contains(mouseupNode) ||
el.contains(mousedownNode)
) return
el[ctx].cb()
}
export default {
mounted (el, binding) {
// 添加进监听列表,同时给元素上添加处理时需要的属性
nodeList.push(el)
el[ctx] = {
cb: binding.value,
id: seed++
}
},
updated (el, binding) {
// 重置元素上的属性
el[ctx] = {
cb: binding.value,
id: seed++
}
},
unmounted (el) {
// 先从监听列表中移除该元素
for (let [index, item] of nodeList.entries()) {
if (item.id === el[ctx].id) {
nodeList.splice(index, 1)
break
}
}
// 删除该元素上的属性
delete el[ctx]
}
}

实现点击非目标指令
https://xin-fas.github.io/2023/09/17/实现点击非目标指令/
作者
Xin-FAS
发布于
2023年9月17日
更新于
2023年11月5日
许可协议