基本原理:
事件的冒泡 和捕获 ,你只要不阻止冒泡 ,事件就会传递到document
export const clickoutside = {
// 初始化指令
bind(el, binding, vnode) {
function documentHandler(domEvent) {
debugger
//js的contains方法用来查看dom元素的包含关系,
//如果 点击的触发元素 domEvent.target ,不是指令使用元素(el)的子元素 ,啥也不干
if (el.contains(domEvent.target)) {
return false;
}
//如果是子元素 而且绑定了回调方法
/*
arg:传给指令的参数
expression:字符串形式的指令表达式
*/
if (binding.expression) {
// v-clickoutside:[obj]="handleClickoutside" obj 就是参数
if (binding.arg) {
if (typeof binding.arg == 'object') {
binding.value(binding.arg);
} else {
binding.value(domEvent, binding.arg);
}
return
}
binding.value(domEvent);
}
}
el.__vueClickOutside__ = documentHandler;
//将 点击事件 放在全局
document.addEventListener('click', documentHandler);
},
unbind(el, binding) {
document.removeEventListener('click', el.__vueClickOutside__);
delete el.__vueClickOutside__;
},
};
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<style>
body {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
background: #cccccc;
}
.content {
width: 200px;
height: 100px;
border: 1px solid #333333;
cursor: pointer;
margin: 0 auto;
}
</style>
</head>
<body>
<div id="counter">
<div class="content" @click="handleClick" v-clickoutside:[obj]="handleClickoutside"> counter:{{ counter }} </div>
<!-- <div @click="handleClick" v-clickoutside="handleClickoutside"> counter:{{ counter }} </div> -->
</div>
</body>
<script type="module">
import { clickoutside } from './clickoutside2.js'
const Counter = {
data() {
return {
obj:{name:'jkj'},
counter: 0
}
},
methods: {
handleClickoutside(e) {
console.log('点击了外侧');
},
handleClick(e) {
// console.log(111, e);
}
}
}
const app = new Vue(Counter)
// app.mount('#counter')
Vue.directive('clickoutside', clickoutside)
app.$mount('#counter')
</script>
</html>
elementUI 的实现
import Vue from 'vue';
import { on } from 'element-ui/src/utils/dom';
const nodeList = [];
const ctx = '@@clickoutsideContext';
let startClick;
let seed = 0;
!Vue.prototype.$isServer && on(document, 'mousedown', e => (startClick = e));
!Vue.prototype.$isServer && on(document, 'mouseup', e => {
nodeList.forEach(node => node[ctx].documentHandler(e, startClick));
});
function createDocumentHandler(el, binding, vnode) {
return function(mouseup = {}, mousedown = {}) {
if (!vnode ||
!vnode.context ||
!mouseup.target ||
!mousedown.target ||
el.contains(mouseup.target) ||
el.contains(mousedown.target) ||
el === mouseup.target ||
(vnode.context.popperElm &&
(vnode.context.popperElm.contains(mouseup.target) ||
vnode.context.popperElm.contains(mousedown.target)))) return;
if (binding.expression &&
el[ctx].methodName &&
vnode.context[el[ctx].methodName]) {
vnode.context[el[ctx].methodName]();
} else {
el[ctx].bindingFn && el[ctx].bindingFn();
}
};
}
/**
* v-clickoutside
* @desc 点击元素外面才会触发的事件
* @example
* ```vue
* <div v-element-clickoutside="handleClose">
* ```
*/
export default {
bind(el, binding, vnode) {
nodeList.push(el);
const id = seed++;
el[ctx] = {
id,
documentHandler: createDocumentHandler(el, binding, vnode),
methodName: binding.expression,
bindingFn: binding.value
};
},
update(el, binding, vnode) {
el[ctx].documentHandler = createDocumentHandler(el, binding, vnode);
el[ctx].methodName = binding.expression;
el[ctx].bindingFn = binding.value;
},
unbind(el) {
let len = nodeList.length;
for (let i = 0; i < len; i++) {
if (nodeList[i][ctx].id === el[ctx].id) {
nodeList.splice(i, 1);
break;
}
}
delete el[ctx];
}
};