4.1 Sensors -- onClickOutside

4.1 Sensors – onClickOutside

https://vueuse.org/onClickOutside

作用

监听当前的点击是否是在目标元素之外。在弹窗和下拉框中非常有用。

官方示例

<template>
  <div ref="target">
    Hello world
  </div>
  <div>
    Outside element
  </div>
</template>

<script>
import { ref } from 'vue'
import { onClickOutside } from '@vueuse/core'

export default {
  setup() {
    const target = ref(null)
    onClickOutside(target, (event) => console.log(event))
    return { target }
  }
}
</script>

  • 无渲染组件的代码如下,通过@trigger触发回调
<OnClickOutside @trigger="count++" :options="{ ignore: [/* ... */] }">
  <div>
    Click Outside of Me
  </div>
</OnClickOutside>
  • 指令用法,指令放在被监控元素上
<script setup lang="ts">
import { ref } from 'vue'
import { vOnClickOutside } from '@vueuse/components'

const modal = ref(false)
function closeModal() {
  modal.value = false
}
</script>

<template>
  <button @click="modal = true">
    Open Modal
  </button>
  <div v-if="modal" v-on-click-outside="closeModal">
    Hello World
  </div>
</template>

源码分析

我们自己实现的时候,一般都是判断鼠标点击的元素以及它的所有父元素,和目标元素是否有交集,通过递归el.parentNode实现。

看一下源码的实现。

  1. cleanup=[],其中的useEventListener返回的都是取消监听的函数。
  2. 监听了click、pointerdown、pointer事件,我们忽略iframe的情况。
  3. click事件始终触发handler回调,除非点击了忽略的元素。
  4. pointerdown判断鼠标按下,修改shouldListen,判断起点是否在target之外。
  5. pointerup回调listener事件,判断终点是否在target之外,同时又shouldListen,所以是否触发回调是二者共同控制的,只有起点和终点都在元素外才可以触发。

⚠️:思考一些问题:

1、为啥注册click、pointerdown、pointerup这么多事件?

​ 如果是鼠标左键点击: pointerdown --> pointerup --> click,如果是鼠标右键点击: pointerdown --> pointerup ,如果是拖动,也不会触发click,所以只监听click是不够的。

2、回调会重复触发吗?

​ 不会,因为pointerup的回调是fallback = window.setTimeout(() => listener(e), 50),所以如果触发了click,会执行window.clearTimeout(fallback)。所以只有click真正触发了回调。

3、右键点击是不是也会触发回调?

​ 从以上的结论来看,右键不会触发click,同时pointerup又限制了e.button === 0,只有左键起作用,所以右键不会触发回调。

export function onClickOutside<T extends OnClickOutsideOptions>(
  target: MaybeElementRef,
  handler: OnClickOutsideHandler<{ detectIframe: T['detectIframe'] }>,
  options: T = {} as T,
) {
  const { window = defaultWindow, ignore = [], capture = true, detectIframe = false } = options

  if (!window)
    return

  let shouldListen = true

  let fallback: number

  /**
  * 判断当前这次事件是否应该被忽略
  */
  const shouldIgnore = (event: PointerEvent) => {
    // 如果用户传递ingore数组中有一项满足条件,这个就返回true
    return ignore.some((target) => {
      // 如果传的是字符串,就找到所有符合条件的dom元素。然后一项项进行比较。
      // 假如其中有一个dom满足:这个dom就是点击的dom,或者这个dom在被点击dom的冒泡路径上(也就是这个dom被鼠标冒泡路径上某一个dom包含)
      if (typeof target === 'string') {
        return Array.from(window.document.querySelectorAll(target))
          .some(el => el === event.target || event.composedPath().includes(el))
      }
      else {
        // 如果传递的是dom代理,直接比较
        const el = unrefElement(target)
        return el && (event.target === el || event.composedPath().includes(el))
      }
    })
  }

  const listener = (event: PointerEvent) => {
    window.clearTimeout(fallback)

    const el = unrefElement(target)
		// 如果点击了target元素或者target的子元素
    if (!el || el === event.target || event.composedPath().includes(el))
      return
    
		// 对于click事件,detail就是1;对于mousedown和mouseup,detail每次都+1;其他事件都是0
    if (event.detail === 0)
      shouldListen = !shouldIgnore(event)

    if (!shouldListen) {
      shouldListen = true
      return
    }
		
    // 点击了外部元素,且本次事件不被忽略,则触发用户传递的回掉
    handler(event)
  }

  const cleanup = [
    useEventListener(window, 'click', listener, { passive: true, capture }),
    
    useEventListener(window, 'pointerdown', (e) => {
      const el = unrefElement(target)
      if (el)
        // 按下时,鼠标不在target之内,则shouldListen=true
        shouldListen = !e.composedPath().includes(el) && !shouldIgnore(e)
    }, { passive: true }),
    
    useEventListener(window, 'pointerup', (e) => {
      
      // e.button === 0 表示鼠标左键
      if (e.button === 0) {
        const path = e.composedPath()
        e.composedPath = () => path
        fallback = window.setTimeout(() => listener(e), 50)
      }
    }, { passive: true }),
    
    detectIframe && useEventListener(window, 'blur', (event) => {
      const el = unrefElement(target)
      if (
        window.document.activeElement?.tagName === 'IFRAME'
        && !el?.contains(window.document.activeElement)
      )
        handler(event as any)
    }),
  ].filter(Boolean) as Fn[]

  const stop = () => cleanup.forEach(fn => fn())

  return stop
}
  • 8
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值