实现点击空白区域隐藏下拉弹窗

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/aaaaaaliang/article/details/89286603

背景


中后台项目依赖Element UI 组件库,组件库中Select 选择器、Popover 弹出框等组件弹出内容后,点击组件外部区域都可以让内容收回。

但是我们的页面中有iframe标签,点击iframe标签时,弹出内容并没有收回。

为了解决这一个问题,我们需要调研一下Element UI 是如何实现这个功能的。

功能描述


点击下图中的输入框 A,显示弹窗B,点击 A B 以外的区域,弹窗B 收回。
在这里插入图片描述

思路


需求点

  1. 实现select弹出框,能让弹出框显示、隐藏切换
  2. 实现一个点击组件外部区域,能通知到弹出窗状态切换的功能
  3. 可以复用,通用。

思路一:利用CSS伪类:focus :focus-within 通过样式控制

1. 兼容性在这里插入图片描述
在这里插入图片描述
2. 是否支持Iframe
支持

3. 示例代码

<input class="select" type="text"> <div class="pop-wrap">下拉框</div>
.select:focus + .pop-wrap {
    display: block;
}

4. 备注
不容易封装,对组件结构有要求,必须按照规定的写法写,很难实现即插即用。
组件html布局比较固定,常规弹窗类组件应该遵循单例模式,这里无法做到。
点击弹出内容区域的时候,焦点会失去,需要单独处理。

思路二:监听focus聚焦、blur失焦事件实现

监听document的聚焦事件,通过聚焦发生时的target来控制弹出哪一个弹窗。

1. 兼容性
在这里插入图片描述
focus事件不支持冒泡模式,需要用捕获模式监听。

2. 是否支持Iframe
支持

3. 示例代码

// focus事件监听:
document.addEventListener('focus', handleFn, true);
// focusin事件监听
document.addEventListener('focusin', handleFnr);

4. 备注
其实用聚焦事件与用mouseup事件的处理方式相似,区别在于点击浏览器之外的区域或点击Iframe时,失焦事件会触发,但是mouseup事件不会触发,这也就导致了用mouseup事件在部分场景会失效。
Element UI 采用的方案就是mouseup,至于如何解决mouseup方案带来的问题,接下来会说明。

事件触发的流程:mouseup事件 → focus事件/blur事件 → 事件处理函数 → 弹出/隐藏弹窗

focus/blur事件虽然解决了点击iframe不触发mouseup的问题,但是相比mouseup而言过程有点不可控,mouseupfocus这一步有时候不受控制。

例如:
在页面中聚焦事件后打开弹窗,这个时候你切换应用,这时候会触发blur事件,回到浏览器窗口时又触发了focus事件,这两步操作其实是多余的。

当然如果你正需要做一个当焦点不在你的网页上时,显示一个蒙层来保证隐私,focus是一个很好的选择。

思路三:监听mouseup事件实现

监听documentmouseup事件发生时的target来控制弹出哪一个弹窗

1. 兼容性

2. 是否支持
iframe

3. 示例代码

// mouseup事件监听:
document.addEventListener('mouseup', handleFn);

4. 备注
mouseup事件处理弹窗的弹出与隐藏时,整个过程更加可控。
事件触发的流程:mouseup事件 → focus事件 → 事件处理函数 → 弹出/隐藏弹窗
相比focus省略了一步,整个流程更可控了,但是无法处理浏览器之外的情况。

方案分析


确定了具体的方案后,我们需要确定实现该方案的主要逻辑

  1. 有一个下拉选择组件,他们提供handleSelectOpen 和 handleSelectClose 方法;
  2. 有一个主题对象,用来收集 组件,主题对象拥有触发 组件们 执行 handleSelectClose的能力
  3. 往主题对象添加组件的能力
  4. 发布事件消息的能力,能通知主题对象执行的能力

其实就是一个观察者模式的实践。
在这里插入图片描述

Element UI 的案例分析


v-clickoutside,一个自定义指令。

v-clickoutside的结构

import { on } from 'element-ui/src/utils/dom';
 
const nodeList = [];
const ctx = '@@clickoutsideContext';
let startClick;
  
// 发布消息
!Vue.prototype.$isServer && on(document, 'mousedown', e => (startClick = e));
!Vue.prototype.$isServer && on(document, 'mouseup', e => {
  nodeList.forEach(node => node[ctx].documentHandler(e, startClick));
});
 
// 创建document处理函数
function createDocumentHandler(el, binding, vnode) {
  return function(mouseup = {}, mousedown = {}) {
    // doSomething()...
  };
}
 
export default {
  bind(el, binding, vnode) {
    // 推入组件,至添加主题对象
  },
 
  update(el, binding, vnode) {
    // 更新组件
  },
 
  unbind(el) {
    // 推出组件,在主体对象中删除
  }
};

创建处理document上鼠标点击事件的函数

// 闭包的作用,缓存变量
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();
    }
  };
}

收集组件,添加至主题对象

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) {
      	// 从主体对象中移除el即可
        nodeList.splice(i, 1);
        break;
      }
    }
    delete el[ctx];
  }

如何解决iframe存在时无法监听mouseup的问题


现在有一个页面,称之为父页面。父页面里面有一个iframe,firame里的页面称之为子页面。
我们需要解决点击子页面时,父页面内的select组件的弹窗内容可以收回。

方案一:父子页面间通信

子页面监听点击事件后,通知父页面。父页面使用上面所说的主题对象的方法来关闭弹窗。
因此问题就变成了父子页面间如何通信。

  1. postMessage 可以跨域,但需要做白名单处理,且对子页面有侵入
    https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage
    // 子页面发布消息 
    otherWindow.postMessage(message, targetOrigin, [transfer]);
    
    // 父页面接口消息
    window.addEventListener("message", receiveMessage,false); 
    
  2. 通过iframe.contentDocument属性添加监听方法
    不能跨域,使用方式简单
    // 在父页面中怎么监听iframe的事件
    var sonDocument = document.getElementsByTagName("iframe")[0].contentWindow.document   
    sonDocument.addEventListener('mouseup', function(e){
        console.log('mouseup',e) 
    });
    
  3. 其他未知方法

优缺点:
好处:性能比较好,父子间通信的内容也可以自定义,很灵活,不仅仅能通知点击操作。在同域的情况下,用iframe.contentDocument实现监听非常方便
坏处:在不同域的情况下,实现监听比较麻烦,对子页面有侵入。

方案二:document.activeElement

document.activeElement方法能够返回当前页面中获得焦点的元素
document.activeElement MDN文档

借助于这个方法,我们可以获取当前的焦点在不在iframe上,如果焦点在iframe上,使用上面所说的主题对象的方法来关闭弹窗。

// 每隔200毫秒判断当前焦点在不在iframe上
setInterval(function() {
    let activeEle = document.activeElement
    if(activeEletagName ==="IFRAME"){
        // 焦点在iframe时
        // doSomething()
    }
}, 200);

优缺点:
好处:对iframe页面没有任何侵入,父页面可以自己实现功能,不依赖与iframe内的页面。
坏处:需要不停地获取document.activeElement 的状态,理论上对性能有影响,具体影响有多大还需要测试
`

总结

为了实现点击iframe也能关闭弹窗,除了鼠标事件mouseupmousedown的时候触发通知,还需要在点击iframe的时候也触发通知。至于是通过父子间通信实现,还是通过activeElement监听实现,可以根据业务场景选择。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/aaaaaaliang/article/details/89286603