注:(1)非原创,来自https://blog.csdn.net/weixin_33985679/article/details/89699215https://zhuanlan.zhihu.com/p/38392987

(2)focus-outside的github地址:https://github.com/txs1992/focus-outside、使用说明文档:https://github.com/txs1992/focus-outside/releases的reademe

为什么无法触发 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 库

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 引入的,应该这样使用
// <script src="https://unpkg.com/focus-outside@0.5.0/lib/index.js"></script>
// const { bind: focusBind, unbind: focusUnbind } = FocusOutside const 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 函数时候会将类名从元素上删除 。

<div id="focus-ele"></div>

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

在 Vue 中使用

// outside.js
export default {
bind (el, binding) {
focusBind(el, binding.value)
}, unbind (el, binding) {
focusUnbind(el, binding.value)
}
} // xx.vue
<template>
<div v-outside="handleOutside"></div>
</template> <script>
import outside from './outside.js' export default {
directives: { outside }, methods: {
handleOutside () {
// 做点什么...
}
}
}
</script>

查看在线示例

在 Element 中使用

<tempalte>
<el-dropdown
ref="dropdown"
trigger="click">
<span class="el-dropdown-link">
下拉菜单<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu
ref="dropdownContent"
slot="dropdown">
<el-dropdown-item>黄金糕</el-dropdown-item>
<el-dropdown-item>狮子头</el-dropdown-item>
<el-dropdown-item>螺蛳粉</el-dropdown-item>
<el-dropdown-item>双皮奶</el-dropdown-item>
<el-dropdown-item>蚵仔煎</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template> <script>
import { bind: focusBind, unbind: focusUnbind } from 'focus-outside' export default {
mounted () {
focusBind(this.$refs.dropdown.$el, this.$refs.dropdown.hide)
focusBind(this.$refs.dropdownContent.$el, this.$refs.dropdown.hide)
}, destoryed () {
focusUnbind(this.$refs.dropdown.$el, this.$refs.dropdown.hide)
focusUnbind(this.$refs.dropdownContent.$el, this.$refs.dropdown.hide)
}
}
</script>

查看在线示例

在 Ant Design 中使用

import { Menu, Dropdown, Icon, Button } from 'antd'
import { bind: focusBind, unbind: focusUnbind } from 'focus-outside' function getItems () {
return [1,2,3,4].map(item => {
return <Menu.Item key={item}>{item} st menu item </Menu.Item>
})
} class MyMenu extends React.Component {
constructor (props) {
super(props)
this.menuElm = null
} render () {
return (<Menu ref="menu" onClick={this.props.onClick}>{getItems()}</Menu>)
} 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 = (<MyMenu outside={ this.handleOutside } onClick={ this.handleClick } />)
return (
<Dropdown
ref="divRef"
visible={this.state.visible}
trigger={['click']}
overlay={ menu }>
<Button style={{ marginLeft: 8 }} onClick={ this.handleClick }>
Button <Icon type="down" />
</Button>
</Dropdown>
)
} 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(
<MyDropdown/>,
document.getElementById('container')
)

查看在线示例

总结

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

如何解决 iframe 无法触发 clickOutside的更多相关文章

  1. CP="CAO PSA OUR" 用P3P header解决iframe跨域访问cookie

    1.IE浏览器iframe跨域丢失Session问题 在开发中,我们经常会遇到使用Frame来工作,而且有时是为了跟其他网站集成,应用到多域的情况下,而Iframe是不能保存Session的因此,网上 ...

  2. 解决iframe缓存机制导致页面不清除缓存不刷新页面的bug

    在使用iframe时,已有页面嵌套了一个iframe页面,当这个页面提交后再次跳转到本页面时,原本iframe内的页面应该刷新数据的,结果未刷新,需要清除缓存后才刷新. 解决方案: var fresh ...

  3. 真正解决iframe高度自适应问题

    1.前言 解决iframe高度自适应问题有两种方法1.pym2.手动设置iframe的高度 本文主要是总结第二种实现方式,因为第一种pym.js插件我没用懂 如果使用iframe时,遇到以下的需求: ...

  4. 解决iframe作为子窗口,刷新后iframe页面跳转到其它页面的问题

    转载请在页首注明作者与出处 http://www.cnblogs.com/zhuxiaojie/p/5990262.html 前言: 在开发网站时,尤其是管理后台,我们经常会使用iframe作为内容窗 ...

  5. IE8利用setCapture和releaseCapture解决iframe的拖拽事件

    最近有个需求须要实现左右拖拽功能,页面右边是个iframe页面,在chrome测试通过之后,发现在ie8上面效果不是很理想,最后查找资料得知可以使用ie自带的setCapture和releaseCap ...

  6. Selenium UI自动化解决iframe定位问题

      更多原创测试技术文章同步更新到微信公众号 :三国测,敬请扫码关注个人的微信号,感谢! 原文链接:http://www.cnblogs.com/zishi/p/6735116.html 一个阴雨霏霏 ...

  7. 解决iframe在移动端(主要iPhone)上的问题

    前言 才发现已经有一段时间没有写博客了,就简单的说了最近干了啥吧.前段时间忙了杂七杂八的事情,首先弄了个个人的小程序,对的,老早就写了篇从零入手微信小程序开发,然后到前段时间才弄了个简单的个人小程序, ...

  8. Nginx反向代理解决iframe跨域问题

    前言 这几天有个需求:做个表单页面,要求后台人员能自定义发布表单,用户来填写表单.我一想,这不麦克表单有现成的吗,拿来就用!发布表单后,可以选择使用iframe方式嵌入网站,一切顺利. 当时的网站是h ...

  9. 自动化测试系列:Selenium UI自动化解决iframe定位问题

      更多原创测试技术文章同步更新到微信公众号 :三国测,敬请扫码关注个人的微信号,感谢! 原文链接:http://www.cnblogs.com/zishi/p/6735116.html 一个阴雨霏霏 ...

随机推荐

  1. PHP natcasesort() 函数

    定义和用法 natcasesort() 函数用"自然排序"算法对数组进行排序.键值保留它们原始的键名. 在自然排序算法中,数字 2 小于 数字 10.在计算机排序算法中,10 小于 ...

  2. PHP fgetcsv() 函数

    定义和用法 fgetcsv() 函数从打开的文件中解析一行,校验 CSV 字段. fgetcsv() 函数会在到达指定长度或读到文件末尾(EOF)时(以先到者为准),停止返回一个新行. 该函数如果成功 ...

  3. PHP ezmlm_hash() 函数

    定义和用法 ezmlm_hash() 函数用于在 MySQL 数据库中保存 EZMLM 邮件列表的哈希值. 该函数接收一个 Email 地址参数,返回一个整数哈希值. 语法 int ezmlm_has ...

  4. PHP zip_open() 函数

    定义和用法 zip_open() 函数打开 zip 档案以供读取.高佣联盟 www.cgewang.com 如果成功,该函数则返回 zip 文件资源.如果失败,则返回 FALSE. 语法 zip_op ...

  5. ElasticSearch实战系列六: Logstash快速入门和实战

    前言 本文主要介绍的是ELK日志系统中的Logstash快速入门和实战 ELK介绍 ELK是三个开源软件的缩写,分别表示:Elasticsearch , Logstash, Kibana , 它们都是 ...

  6. MySQL一主多从配置和读写分离配置

    一.一主多从配置 此次操作实现的是一主两从的方式.主服务器slave2(2.100),从服务器slave2-1(2.107),slave2-2(2.108);第一:准备主数据库    1. 在不同的机 ...

  7. ElasticSearch添加索引

    1. 编写索引内容 节点解释: settings:配置信息 "number_of_replicas": 0  不需要备份(单节点的ElasticSearch使用) "ma ...

  8. 细说selenium的等待条件

    selenium的显示等待 在进行UI自动化测试的时候,我们为了保持用例的稳定性,往往要设置显示等待,显示等待就是说明确的要等到某个元素的出现或者元素的某些条件出现,比如可点击.可见等条件,如果在规定 ...

  9. Navicat15安装教程

    本文内容皆为作者原创,如需转载,请注明出处:https://www.cnblogs.com/xuexianqi/p/12797170.html 一:简介 Navicat是一套快速.可靠的数据库管理工具 ...

  10. ThreadLocal刨根问底

    一.ThreadLocal使用场景 数据库连接connection对象使用,每个客户都能使用自己的connection对象.不会出现客户A操作关闭了客户B的connection 案例:https:// ...