zoukankan      html  css  js  c++  java
  • 如何解决 iframe 无法触发 clickOutside

    注:(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。

  • 相关阅读:
    一张图片入门Python
    4.1. 如何在Windows环境下开发Python
    你必须知道的EF知识和经验
    XUnit的使用
    如何使用NUnit
    Entity Framework 不支持DefaultValue
    Have You Ever Wondered About the Difference Between NOT NULL and DEFAULT?
    Validation failed for one or more entities. See 'EntityValidationErrors' property for more details
    Entity Framework 与多线程
    sqlite中的自增主键
  • 原文地址:https://www.cnblogs.com/mark21/p/13503271.html
Copyright © 2011-2022 走看看