V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐关注
Meteor
JSLint - a JavaScript code quality tool
jsFiddle
D3.js
WebStorm
推荐书目
JavaScript 权威指南第 5 版
Closure: The Definitive Guide
mt1992
V2EX  ›  JavaScript

一个很棒的 click outside 解决方案

  •  2
     
  •   mt1992 · 2020-06-12 13:42:15 +08:00 · 941 次点击
    这是一个创建于 1643 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    在公司的一次小组分享会上,组长 给我们分享了一个他在项目中遇到的一个问题。在一个嵌入 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 后,当它获取焦点浏览器会给它加上高亮样式,如果不需要这种样式可以将 outline 设置为 none 。

    不过这种方法虽然很棒,但是也存在一些问题,浏览器兼容性,下面是 MDN 给出的浏览器兼容情况,Firefox 低版本不兼容。

    使用 focus-outside 库

    focus-outside 是我为了解决上述问题所创建的仓库。使用起来也非常方便,它只有两个方法,bind 与 unbind,它不依赖任何其他库,并且支持为多个元素绑定一个函数。

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

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

    并且 focus-outside 1.x 版本支持了 key 属性,可以通过 key 将一组不同的函数和元素绑定在一起。

    https://github.com/txs1992/focus-outside

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   4957 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 08:35 · PVG 16:35 · LAX 00:35 · JFK 03:35
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.