Vue 点击弹窗外部,实现弹窗关闭?你有实现的思路吗?

开发
记得以前面试官问过我一个问题:我现在有一个弹窗,怎样才能实现点击弹窗以外的区域,实现关闭弹窗呢?

背景

记得以前面试官问过我一个问题:我现在有一个弹窗,怎样才能实现点击弹窗以外的区域,实现关闭弹窗呢?

当时确实比较菜,没想出应该怎么做才行,因为当时我的脑子里只有 click事件,我在想点击事件不是只能绑定在本元素身上吗?怎么才能点击其他地方来影响本元素呢?

实现思路

过了一两年后,回头发现,其实实现并不困难,很多人其实也都会,换一种说法,面试官想问的是:在 Vue 中,有一个元素X,怎么做到点击元素X以外的东西,触发绑定在元素X上的事件。

我把实现思路分为几步:

  • 定义一个 Map,来收集弹窗元素
  • 监听 document 的鼠标按下、松开事件
  • document 鼠标按下时记录触发的元素A
  • document 鼠标松开时遍历 Map 中所有弹窗元素,让这些弹窗元素跟元素A一一比较,不等于则说明是点了弹窗元素外部,等于则说明点击了弹窗元素内部
  • 点击了外部则触发绑定事件,点击了内部则不触发

ClickOutside

其实上面的思路,就是 v-clickoutside的实现思路,这个自定义指令,是 Vue 中用的非常广泛的指令~具体用法是这样的:

cs () {
  console.log('点击外部')
}

<div v-clickoutside="cs"></div>

<button>点我</button>
<button>哈哈哈</button>

当你点击了 div 元素,也就是本元素,并不会触发事件 cs,而当你点击它以外的元素,则会触发 cs 事件。

代码实现

1.TypeScript类型准备

// vue自带的一些类型
import type { ComponentPublicInstance, DirectiveBinding, ObjectDirective } from 'vue';

// 下面会用到,是记录绑定事件的函数
type DocumentHandler = <T extends MouseEvent>(mouseup: T, mousedown: T) => void;

// Map 的类型
// key 是元素本地
// value 是绑定的事件
type FlushList = Map<
  HTMLElement,
  DocumentHandler
>;

2.绑定事件函数

首先封装一个绑定事件的函数,大家在平时封装函数的时候一定要注意判空,兜底~

export function on(
  element: Element | HTMLElement | Document | Window,
  event: string,
  handler: EventListenerOrEventListenerObject,
): void {
  if (element && event && handler) {
    element.addEventListener(event, handler, false);
  }
}

3.判断点击元素是否是本元素

想一想我们的目的是啥,有一元素A,我需要点击元素A以外的地方才触发绑定的事件,点击元素A或者元素A以内的区域则不触发

所以这个函数主要做几件事:

  • 判断点击的元素是否是本元素(不触发)
  • 判断点击的元素是否在本元素内(不触发)
  • 兜底,判断元素是否存在(触发)
function createDocumentHandler(el: HTMLElement, binding: DirectiveBinding): DocumentHandler {
  return function (mouseup, mousedown) {
    const mouseUpTarget = mouseup.target as Node;
    const mouseDownTarget = mousedown.target as Node;
    const isBound = !binding || !binding.instance;
    const isTargetExists = !mouseUpTarget || !mouseDownTarget;
    const isContainedByEl = el.contains(mouseUpTarget) || el.contains(mouseDownTarget);
    const isSelf = el === mouseUpTarget;

    if (
      isBound ||
      isTargetExists ||
      isContainedByEl ||
      isSelf
    ) {
      return;
    }
    binding.value();
  };
}

4.自定义指令

自定义指令的几个生命周期里,需要做这些事:

  • 绑定时,记录绑定元素与绑定事件到 nodeList 中
  • 更新时,记录绑定元素与绑定事件到 nodeList 中
  • 销毁时,将此元素从 nodeList 中抹除
// 记录绑定元素的 Map
const nodeList: FlushList = new Map();

const ClickOutside: ObjectDirective = {
  beforeMount(el, binding) {
    nodeList.set(el, 
      createDocumentHandler(el, binding));
  },
  updated(el, binding) {
    nodeList.set(el, 
      createDocumentHandler(el, binding));
  },
  unmounted(el) {
    nodeList.delete(el);
  },
};

export default ClickOutside;

5.监听 document 鼠标按下、松开

万事俱备只欠东风,现在只需要监听 document 的鼠标按下、松开事件 即可,大概分为几步:;

  • 鼠标按下时,记录这个触发的元素
  • 鼠标松开时,遍历 nodeList 中的元素,跟这个触发元素做对比
  • 符合条件则执行绑定事件,不符合则不执行
let startClick: MouseEvent;

on(document, 'mousedown', (e: MouseEvent) => (startClick = e));
on(document, 'mouseup', (e: MouseEvent) => {
  for (const { documentHandler } of nodeList.values()) {
    documentHandler(e, startClick);
  }
});

这就实现了点击外部触发内部事件的效果了!

责任编辑:赵宁宁 来源: 前端之神
相关推荐

2021-08-13 22:26:40

Windows 10Windows微软

2021-06-21 15:49:39

React动效组件

2024-04-15 09:22:48

CSS锁定overflow

2023-01-29 09:46:47

Dialog弹窗模态

2009-07-27 14:29:31

ASP.NET编程弹窗报警提示

2022-10-08 00:41:24

汽车弹窗用户

2021-05-06 13:45:13

PythonExce弹窗

2021-04-19 11:20:25

Windows 10输入法微软

2022-04-04 16:53:56

Vue.js设计框架

2022-07-14 10:38:39

动态标签Spring

2017-10-20 13:17:21

2022-08-01 12:53:30

前端动画

2015-09-02 11:22:36

JavaScript实现思路

2020-05-07 09:45:16

前端JS图片压缩

2024-06-20 08:42:45

2020-11-18 13:24:02

广告弹窗网信互联网

2020-07-24 09:20:21

弹窗开机游戏

2020-05-25 16:09:17

苹果iOSBug

2021-08-04 09:31:06

Chrome破坏性功能
点赞
收藏

51CTO技术栈公众号