博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
如何解决 iframe 无法触发 clickOutside
阅读量:6786 次
发布时间:2019-06-26

本文共 5134 字,大约阅读时间需要 17 分钟。

前言

在公司的一次小组分享会上, 给我们分享了一个他在项目中遇到的一个问题。在一个嵌入 iframe 的系统中,当我们点击按钮展开 Dropdown 展开后,再去点击 iframe 发现无法触发 Dropdown 的 clickOutside 事件,导致 Dropdown 无法关闭。

为什么无法触发 clickOutside

目前大多数的 UI 组件库,例如 Element、Ant Design、iView 等都是通过鼠标事件来处理, 下面这段是 iView 中的 clickOutside 代码,iView 直接给 Document 绑定了 click 事件,当 click 事件触发时候,判断点击目标是否包含在绑定元素中,如果不是就执行绑定的函数。

bind (el, binding, vnode) {  function documentHandler (e) {    if (el.contains(e.target)) {      return false;    }    if (binding.expression) {      binding.value(e);    }  }  el.__vueClickOutside__ = documentHandler;  document.addEventListener('click', documentHandler);}复制代码

但 iframe 中加载的是一个相对独立的 Document,如果直接在父页面中给 Document 绑定 click 事件,点击 iframe 并不会触发该事件。

知道问题出现在哪里,接下来我们来思考怎么解决?

给 iframe 的 body 元素绑定事件

我们可以通过一些特殊的方式给 iframe 绑定上事件,但这种做法不优雅,而且也是存在问题的。我们来想想一下这样一个场景,左边是一个侧边栏(导航栏),上面是一个 Header 里面有一些 Dropdown 或是 Select 组件,下面是一个页面区域。

但这些页面有的是嵌入 iframe,有些是当前系统的页面。如果使用这种方法,我们在切换路由的时候就要不断的去判断这个页面是否包含 iframe,然后重新绑定/解绑事件。而且如果 iframe 和当前系统不是同域(大多数情况都不是同域的),那么这种做法是无效的。

添加遮罩层

我们可以通过给 iframe 添加一个透明遮罩层,点击 Dropdown 的时候显示透明遮罩层,点击 Dropdown 之外的区域或遮罩层,就派发 clickOutside 事件并关闭遮罩层,这样虽然可以触发 clickOutside 事件,但存在一个问题,如果用户点击的区域正好是 iframe 页面中的某个按钮,那么第一次点击是不会生效的,这种做法对于交互不是很友好。

监听 focusin 与 focusout 事件

其实我们可以换一种思路,为什么一定要用鼠标事件来做这件事呢?focusin 与 focusout 事件就很适合处理当前这种情况。

当我们点击绑定的元素之外时就触发 focusout 事件,这时我们可以添加一个定时器,延时调用我们绑定的函数。而当我们点击绑定元素例如 Dropdown 会触发 focusin 事件,这时候我们判断目标是否包含在绑定元素中,如果包含在绑定元素中就清除定时器。

不过使用 focusin 与 focusout 事件需要解决一个问题,那就是要将绑定的元素变成 focusable 元素,那么怎么将元素变成 focusable 元素呢?我们通过将元素的 tabindex 属性置为 -1 , 该元素就变成 focusable 的元素。

需要注意的是,元素变成 focusable 元素之后,当它获取焦点的时候,浏览器会给它加上默认的高亮样式,如果你不需要这种样式可以将 outline 属性设置为 none。

不过这种方法虽然很棒,但是也会存在一些问题,浏览器兼容性,下面是 MDN 给出的浏览器兼容情况,从图中可以看出 Firefox 低版本不支持这个事件,所以你需要去权衡你的项目是否支持低版本的 Firefox 浏览器。

使用 focus-outside 库

 正是为了解决上述问题所创建的仓库,代码不到 200 行。使用起来也非常方便,它只有两个方法,bind 与 unbind,不依赖其他第三方库,并且支持为多个元素绑定同一个函数。

为什么要给多个元素绑定同一个函数,这么做是为了兼容 Element 与 Ant Design,因为 Element 与 Ant Design 会将 Dropdown 插入 body 元素中,它的按钮和容器是分离的,当我们点击按钮显示 Dropdown,当我们点击 Dropdown 区域,这时候按钮会失去焦点触发 focusout 事件。事实上我们并不希望这时关闭 Dropdown,所以我将它们视为同一个绑定源。

这里说明下 Element 与 Ant Design 为什么要将弹出层放在 body 元素中,因为如果直接将 Dropdown 挂载在父元素下,会受到父元素样式的影响。比如当父元素有 overflow: hidden,Dropdown 就有可能被隐藏掉。

简单使用

// import { bind, unbidn } from 'focus-outside'// 建议使用下面这种别名,防止和你的函数命名冲突了。import { bind: focusBind, unbind: focusUnbind } from 'focus-outside'// 如果你是使用 CDN 引入的,应该这样使用// // const { bind: focusBind, unbind: focusUnbind } = FocusOutsideconst elm = document.querySelector('#dorpdown-button')// 绑定函数focusBind(elm, callback)function callback () {  console.log('您点击了 dropdown 按钮外面的区域')  // 清除绑定  focusUnbind(elm, callback)}复制代码

注意

前面说到过元素变成 focusable 元素后,当它获取焦点浏览器会给它加上高亮样式,如果你不希望看到和这个样式,你需要将这个元素的 CSS 属性 outline 设置为 none。focsout-outside 0.5.0 版本中新增 className 参数,为每个绑定的元素添加 focus-outside 默认类名,你要可以通过传递 className 参数自定义类名,当执行 unbind 函数时候会将类名从元素上删除 。

// jsconst elm = document.querySelector('#focus-ele')// 默认类名是 focus-outsidefocusBind(elm, callback, 'my-focus-name')// css// 如果你需要覆盖所有的默认样式,可以在这段代码放在全局 CSS 中。.my-focus-name { outline: none;}复制代码

在 Vue 中使用

// outside.jsexport default {  bind (el, binding) {    focusBind(el, binding.value)  },  unbind (el, binding) {    focusUnbind(el, binding.value)  }}// xx.vue
复制代码

在 Element 中使用

下拉菜单
黄金糕
狮子头
螺蛳粉
双皮奶
蚵仔煎
复制代码

在 Ant Design 中使用

import { Menu, Dropdown, Icon, Button } = antdimport { bind: focusBind, unbind: focusUnbind } = 'focus-outside'function getItems () {  return [1,2,3,4].map(item => {    return 
{item} st menu item
})}class MyMenu extends React.Component { constructor (props) { super(props) this.menuElm = null } render () { return (
{getItems()}
) } componentDidMount () { this.menuElm = ReactDOM.findDOMNode(this.refs.menu) if (this.menuElm && this.props.outside) focusBind(this.menuElm, this.props.outside) } componentWillUnmount () { if (this.menuElm && this.props.outside) focusUnbind(this.menuElm, this.props.outside) }}class MyDropdown extends React.Component { constructor (props) { super(props) this.dropdownElm = null } state = { visible: false } render () { const menu = (
) return (
) } componentDidMount () { this.dropdownElm = ReactDOM.findDOMNode(this.refs.divRef) if (this.dropdownElm) focusBind(this.dropdownElm, this.handleOutside) } componentWillUnmount () { if (this.dropdownElm) focusUnbind(this.dropdownElm, this.handleOutside) } handleOutside = () => { this.setState({ visible: false }) } handleClick = () => { this.setState({ visible: !this.state.visible }) }}ReactDOM.render(
, document.getElementById('container'))复制代码

总结

iframe 元素无法触发鼠标事件,如果在嵌入 iframe 的系统中触发 clickOutside, 更好的做法是使用 focusin 与 focusout 事件,将 HTML 属性 tabindex 设置为 -1 可以将元素变成 focusable 元素。浏览器会给 focusable 元素加上默认的高亮样式,如果你不需要这种样式,可以将 CSS 属性 outline 设置为 none。

相关链接

原文发布时间为:2018年07月02日

作者:掘金

本文来源:
如需转载请联系原作者
你可能感兴趣的文章
简析Uber的可伸缩监控:uMonitor和Neris
查看>>
腾讯云答治茜:云计算为独角兽和传统企业提供了哪些沃土?
查看>>
Spark on YARN 部署案例
查看>>
RedHat发布JBoss 7.2,完全支持Java EE 8规范
查看>>
kubernetes1.9.2基于kubeadm的高可用安装HA
查看>>
「性能优化之道」每秒上万并发下的Spring Cloud参数优化实战
查看>>
App启动流程
查看>>
原理 | 分布式链路跟踪组件 SOFATracer 和 Zipkin 模型转换
查看>>
我的第一篇博客
查看>>
手把手教你如何用Python从PDF文件中导出数据(附链接)
查看>>
维珍银河完成最长距离火箭飞行,下一步剑指太空旅行
查看>>
[Python]attributeError:'module' object has no attribute 'dump'
查看>>
Docker系列教程11-使用Nexus管理Docker镜像
查看>>
业界最全,阿里云混合云灾备服务上线!
查看>>
Windows Linux 子系统可以在资源管理器中打开
查看>>
WebStorm文件类型关联设置
查看>>
13.1 Spring MVC 关于controller的字符编码
查看>>
理发店与 App 定价模型
查看>>
ES6(数组)
查看>>
php simplexml_load_file 函数执行不稳定
查看>>