In general, the most listened to business is Click event, but in some business such as Alert and Pop effect, you need to listen to the click outside the element to close the pop-up window.

Original Implementation

Here are two common ways to implement the modal box

Option 1: The default click is placed in the bubble stage, just add a click on the content area to prevent bubbling

1
2
3
4
<div class="cover" @click="close">
  <!-- 阻止冒泡 -->
  <div class="content" @click.stop>modal content</div>
</div>

Option 2: Determine by code whether the DOM triggered by a click is within the content area

1
2
3
<div class="cover" @click="handleClick">
  <div class="content" ref="content">modal content</div>
</div>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
handleClick (e) {
  let clickOut = true
  let temp = e.target
  do {
    if (temp === this.$refs.content) {
      clickOut = false
      break
    }
    temp = temp.parentElement
  } while (temp !== document.documentElement)
  console.log(clickOut)
}

Command implementation

The above code can solve the fullscreen modal box to close the external area by clicking on it. But there is another kind of popup, the external area of this kind of popup is not in this component, you want to achieve this kind of popup click external area off with the above way two is also possible, just add the handleClick event to the body in the mounted phase, and unbind the click time on the body in the beforeDestroy.

If multiple components need to achieve the effect of closing the external area by clicking on it, you can wrap it with Vue’s directive

Implementing popup windows

1
2
3
<div class="cover">
  <div class="content" v-out-click="close">modal content</div>
</div>

Realize pop-up

1
2
<button @click="popIsShow = true">显示气泡</button>
<div class="pop" v-if="popIsShow" v-out-click="closePop">I'm pop text</div>

The details of the command code are as follows. One thing is that there is no place to store the variables inside the directive, so we have to put them on the DOM. Another thing is that you should add the prefix v- when you use it, you don’t need to bring v- in the name of the directive

1
2
import outClick from './directive/out-click.js'
Vue.directive(outClick.name, outClick)
 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
const KEY_OUT = '_out_click'
const KEY_OUT_EVENT = '_out_click_event'
const KEY_IN = '_in_click'
const KEY_FLAG = '_in_out_flag'

function removeEvent(el, binding, vnode) {
  el.removeEventListener('click', el[KEY_IN], false)
  window.removeEventListener('click', el[KEY_OUT], false)
  delete el[KEY_IN]
  delete el[KEY_OUT]
  delete el[KEY_OUT_EVENT]
  delete el[KEY_FLAG]
}

function initEvent(el, binding, vnode) {
  // setTimeout 0: 忽略点击外部的按钮初始化该组件时,触发的origin click
  setTimeout(() => {
    el[KEY_OUT] = () => outClick(el)
    el[KEY_IN] = () => inClick(el)
    el[KEY_OUT_EVENT] = binding.value
    el.addEventListener('click', el[KEY_IN], false)
    window.addEventListener('click', el[KEY_OUT], false)
  }, 0)
}

function inClick(el) {
  // 通过事件捕获的顺序作为标志位
  // 最好不要使用阻止冒泡来实现,那样会影响其他的click无法触发
  el[KEY_FLAG] = '1'
}

function outClick(el) {
  if (!el[KEY_FLAG] && el[KEY_OUT_EVENT]) {
    el[KEY_OUT_EVENT]()
  }
  delete el[KEY_FLAG]
}

export default {
  name: 'out-click',
  update: (el, binding, vnode) => {
    if (binding.value === binding.oldValue) {
      return
    }
    removeEvent(el, binding, vnode)
    initEvent(el, binding, vnode)
  },
  bind: initEvent,
  unbind: removeEvent
}