检测单击外部元素

83 javascript vue.js

如何检测元素外的点击?我正在使用Vue.js,所以它将在我的模板元素之外.我知道如何在Vanilla JS中做到这一点,但是当我使用Vue.js时,我不确定是否有更合适的方法可以做到这一点?

这是Vanilla JS的解决方案:在div之外的Javascript Detect Click事件

我想我可以使用更好的方式来访问该元素?

Mad*_*ash 130

有我使用的解决方案,它基于Linus Borg的答案,并与vue.js 2.0一起使用

Vue.directive('click-outside', {
  bind: function (el, binding, vnode) {
    el.clickOutsideEvent = function (event) {
      // here I check that click was outside the el and his childrens
      if (!(el == event.target || el.contains(event.target))) {
        // and if it did, call method provided in attribute value
        vnode.context[binding.expression](event);
      }
    };
    document.body.addEventListener('click', el.clickOutsideEvent)
  },
  unbind: function (el) {
    document.body.removeEventListener('click', el.clickOutsideEvent)
  },
});
Run Code Online (Sandbox Code Playgroud)

有小型演示

您可以在https://vuejs.org/v2/guide/custom-directive.html#Directive-Hook-Arguments中找到有关自定义指令以及el,binding,vnode的更多信息.

  • 有没有办法排除外部特定元素?例如,我外面有一个按钮必须打开此元素,因为它触发了这两种方法,所以什么也没有发生。 (6认同)
  • 工作,但在Vue 2.0指令中不再有实例,所以这是未定义的.https://vuejs.org/v2/guide/migration.html#Custom-Directives-simplified.我不知道为什么这个小提琴有效或者这种简化已经完成.(要解决,将"this"替换为"el"以将事件绑定到元素) (5认同)
  • 它可能有效,因为窗口作为“this”传递。我已经确定了答案。感谢您指出此错误。 (4认同)
  • 你能解释一下vnode.context [binding.expression](event); ? (3认同)

Lin*_*org 68

通过设置一次自定义指令可以很好地解决:

Vue.directive('click-outside', {
  bind () {
      this.event = event => this.vm.$emit(this.expression, event)
      this.el.addEventListener('click', this.stopProp)
      document.body.addEventListener('click', this.event)
  },   
  unbind() {
    this.el.removeEventListener('click', this.stopProp)
    document.body.removeEventListener('click', this.event)
  },

  stopProp(event) { event.stopPropagation() }
})
Run Code Online (Sandbox Code Playgroud)

用法:

<div v-click-outside="nameOfCustomEventToCall">
  Some content
</div>
Run Code Online (Sandbox Code Playgroud)

在组件中:

events: {
  nameOfCustomEventToCall: function (event) {
    // do something - probably hide the dropdown menu / modal etc.
  }
}
Run Code Online (Sandbox Code Playgroud)

关于JSFiddle的工作演示以及有关警告的其他信息:

https://jsfiddle.net/Linusborg/yzm8t8jq/

  • 这种方法在Vue.js 2中不再起作用.self.vm.$ emit调用给出了一条错误消息. (43认同)
  • 应该编辑答案以声明它仅适用于 Vue.js 1 (6认同)
  • 使用@blur 也是一种选择,可以更轻松地给出相同的结果: &lt;input @blur="hide"&gt; where hide: function() { this.isActive = false; } (5认同)
  • 我确实使用了vue clickaway,但我认为你的解决方案或多或少都是一样的.谢谢. (3认同)

G'o*_*r N 19

tabindex属性添加到您的组件,以便可以集中精力并执行以下操作:

<template>
    <div
        @focus="handleFocus"
        @focusout="handleFocusOut"
        tabindex="0"
    >
      SOME CONTENT HERE
    </div>
</template>

<script>
export default {    
    methods: {
        handleFocus() {
            // do something here
        },
        handleFocusOut() {
            // do something here
        }
    }
}
</script>
Run Code Online (Sandbox Code Playgroud)

  • 哇!我发现这是最短和最干净的解决方案。也是唯一一个在我的情况下有效的。 (8认同)
  • 只是为了添加到这一点,将 tabindex 设置为 -1 将阻止在您单击元素时出现高亮框,但它仍然允许 div 可聚焦。 (5认同)
  • 杰出的。您可以添加一个引用`&lt;div ref="menuOptions"&gt;`,然后给它焦点`mounted(){this.$refs.menuOptions.focus();}`,这样当您单击外部而无需聚焦时它就会捕获首先 (3认同)
  • 由于某种原因 -1 的 tabindex 不会隐藏我的轮廓,所以我只是在元素的焦点上添加了“outline: none;”。 (2认同)
  • 我有一个问题,因为当我单击聚焦元素中的子元素时,它会触发 focusout 事件 (2认同)
  • 我找到了解决我自己问题的方法。为了防止点击子元素导致父元素关闭,请将您的登录名放入条件条件内的事件回调中: if(!event.currentTarget.contains(event.latedTarget)) { } (2认同)

fre*_*ett 18

对于 Vue 3:

此答案基于 MadisonTrash上面精彩答案,但已更新为使用新的 Vue 3 语法。

Vue 3 现在使用beforeMount代替bind,unmounted代替unbind( src )。

const clickOutside = {
  beforeMount: (el, binding) => {
    el.clickOutsideEvent = event => {
      // here I check that click was outside the el and his children
      if (!(el == event.target || el.contains(event.target))) {
        // and if it did, call method provided in attribute value
        binding.value();
      }
    };
    document.addEventListener("click", el.clickOutsideEvent);
  },
  unmounted: el => {
    document.removeEventListener("click", el.clickOutsideEvent);
  },
};

createApp(App)
  .directive("click-outside", clickOutside)
  .mount("#app");
Run Code Online (Sandbox Code Playgroud)


Jul*_*nec 17

社区中有两个可用于此任务的包(均维护):

  • `vue-clickaway`包完全解决了我的问题.谢谢 (6认同)

小智 13

如果您专门寻找元素外部但仍在父级内部的点击,则可以使用

<div class="parent" @click.self="onParentClick">
  <div class="child"></div>
</div>
Run Code Online (Sandbox Code Playgroud)

我用它来表示模态。


Alm*_*itt 11

我使用 created() 中的函数以稍微不同的方式做到了这一点。

  created() {
      window.addEventListener('click', (e) => {
        if (!this.$el.contains(e.target)){
          this.showMobileNav = false
        }
      })
  },
Run Code Online (Sandbox Code Playgroud)

这样,如果有人在元素外部单击,那么在我的情况下,移动导航将被隐藏。

希望这可以帮助!

  • 注意:此解决方案不会解除绑定,这会以通常不明显的方式出现内存泄漏和其他问题。选择具有解除绑定/卸载功能的解决方案,以便将来进行验证并稳定您的代码。 (2认同)

小智 9

我已经结合了所有答案(包括来自 vue-clickaway 的一行)并提出了这个对我有用的解决方案:

Vue.directive('click-outside', {
    bind(el, binding, vnode) {
        var vm = vnode.context;
        var callback = binding.value;

        el.clickOutsideEvent = function (event) {
            if (!(el == event.target || el.contains(event.target))) {
                return callback.call(vm, event);
            }
        };
        document.body.addEventListener('click', el.clickOutsideEvent);
    },
    unbind(el) {
        document.body.removeEventListener('click', el.clickOutsideEvent);
    }
});
Run Code Online (Sandbox Code Playgroud)

在组件中使用:

<li v-click-outside="closeSearch">
  <!-- your component here -->
</li>
Run Code Online (Sandbox Code Playgroud)


Nar*_*ren 7

Vue 3 对指令进行了重大更改,所有 <Vue3 方法都已更改/更新。如果您想知道如何在 中做到这一点Vue 3,这是代码片段。有关信息,请通过此链接

<div v-click-outside="methodToInvoke"></div>

click-outside.js

export default {
  beforeMount: function (el, binding, vnode) {
    binding.event = function (event) {
      if (!(el === event.target || el.contains(event.target))) {
        if (binding.value instanceof Function) {
          binding.value(event)
        }
      }
    }
    document.body.addEventListener('click', binding.event)
  },
  unmounted: function (el, binding, vnode) {
    document.body.removeEventListener('click', binding.event)
  }
}
Run Code Online (Sandbox Code Playgroud)

main.js添加以下内容

// Directives
import ClickOutside from './click-outside'

createApp(App)
 .directive('click-outside', ClickOutside)
 .use(IfAnyModules)
 .mount('#app')
Run Code Online (Sandbox Code Playgroud)


ben*_*rwb 6

我已经更新了 MadisonTrash 的答案以支持 Mobile Safari(它没有click事件,touchend必须改用)。这还包含一个检查,以便在移动设备上拖动不会触发事件。

Vue.directive('click-outside', {
    bind: function (el, binding, vnode) {
        el.eventSetDrag = function () {
            el.setAttribute('data-dragging', 'yes');
        }
        el.eventClearDrag = function () {
            el.removeAttribute('data-dragging');
        }
        el.eventOnClick = function (event) {
            var dragging = el.getAttribute('data-dragging');
            // Check that the click was outside the el and its children, and wasn't a drag
            if (!(el == event.target || el.contains(event.target)) && !dragging) {
                // call method provided in attribute value
                vnode.context[binding.expression](event);
            }
        };
        document.addEventListener('touchstart', el.eventClearDrag);
        document.addEventListener('touchmove', el.eventSetDrag);
        document.addEventListener('click', el.eventOnClick);
        document.addEventListener('touchend', el.eventOnClick);
    }, unbind: function (el) {
        document.removeEventListener('touchstart', el.eventClearDrag);
        document.removeEventListener('touchmove', el.eventSetDrag);
        document.removeEventListener('click', el.eventOnClick);
        document.removeEventListener('touchend', el.eventOnClick);
        el.removeAttribute('data-dragging');
    },
});
Run Code Online (Sandbox Code Playgroud)


小智 6

export default {
  bind: function (el, binding, vNode) {
    // Provided expression must evaluate to a function.
    if (typeof binding.value !== 'function') {
      const compName = vNode.context.name
      let warn = `[Vue-click-outside:] provided expression '${binding.expression}' is not a function, but has to be`
      if (compName) { warn += `Found in component '${compName}'` }

      console.warn(warn)
    }
    // Define Handler and cache it on the element
    const bubble = binding.modifiers.bubble
    const handler = (e) => {
      if (bubble || (!el.contains(e.target) && el !== e.target)) {
        binding.value(e)
      }
    }
    el.__vueClickOutside__ = handler

    // add Event Listeners
    document.addEventListener('click', handler)
  },

  unbind: function (el, binding) {
    // Remove Event Listeners
    document.removeEventListener('click', el.__vueClickOutside__)
    el.__vueClickOutside__ = null

  }
}
Run Code Online (Sandbox Code Playgroud)


小智 6

I create a div at the end of the body like that:

<div v-if="isPopup" class="outside" v-on:click="away()"></div>
Run Code Online (Sandbox Code Playgroud)

Where .outside is :

.outside {
  width: 100vw;
  height: 100vh;
  position: fixed;
  top: 0px;
  left: 0px;
}
Run Code Online (Sandbox Code Playgroud)

And away() is a method in Vue instance :

away() {
 this.isPopup = false;
}
Run Code Online (Sandbox Code Playgroud)


yan*_*inn 5

这对我有用Vue.js 2.5.2:

/**
 * Call a function when a click is detected outside of the
 * current DOM node ( AND its children )
 *
 * Example :
 *
 * <template>
 *   <div v-click-outside="onClickOutside">Hello</div>
 * </template>
 *
 * <script>
 * import clickOutside from '../../../../directives/clickOutside'
 * export default {
 *   directives: {
 *     clickOutside
 *   },
 *   data () {
 *     return {
         showDatePicker: false
 *     }
 *   },
 *   methods: {
 *     onClickOutside (event) {
 *       this.showDatePicker = false
 *     }
 *   }
 * }
 * </script>
 */
export default {
  bind: function (el, binding, vNode) {
    el.__vueClickOutside__ = event => {
      if (!el.contains(event.target)) {
        // call method provided in v-click-outside value
        vNode.context[binding.expression](event)
        event.stopPropagation()
      }
    }
    document.body.addEventListener('click', el.__vueClickOutside__)
  },
  unbind: function (el, binding, vNode) {
    // Remove Event Listeners
    document.removeEventListener('click', el.__vueClickOutside__)
    el.__vueClickOutside__ = null
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 谢谢你的这个例子。在 vue 2.6 上检查了这一点。有一些修复,在取消绑定方法中,您必须通过此修复一些问题(您忘记了取消绑定方法中的 body 属性): document.body.removeEventListener('click', el.__vueClickOutside__); 如果不是 - 它将导致在每次组件重新创建(页面刷新)后创建多个事件侦听器; (2认同)

Pab*_*sco 5

vue 3 的完整案例

这是一个基于 MadisonTrash 答案的完整解决方案,以及 benrwb 和 fredrivett 对 safari 兼容性和 vue 3 api 更改的调整。

编辑:

下面提出的解决方案仍然有用,并且如何使用仍然有效,但我将其更改为使用document.elementsFromPoint而不是event.contains因为它无法将某些元素(例如<path>svgs 中的标签)识别为子元素。所以正确的指令是这个:

export default {
    beforeMount: (el, binding) => {
        el.eventSetDrag = () => {
            el.setAttribute("data-dragging", "yes");
        };
        el.eventClearDrag = () => {
            el.removeAttribute("data-dragging");
        };
        el.eventOnClick = event => {
            const dragging = el.getAttribute("data-dragging");
            // Check that the click was outside the el and its children, and wasn't a drag
            console.log(document.elementsFromPoint(event.clientX, event.clientY))
            if (!document.elementsFromPoint(event.clientX, event.clientY).includes(el) && !dragging) {
                // call method provided in attribute value
                binding.value(event);
            }
        };
        document.addEventListener("touchstart", el.eventClearDrag);
        document.addEventListener("touchmove", el.eventSetDrag);
        document.addEventListener("click", el.eventOnClick);
        document.addEventListener("touchend", el.eventOnClick);
    },
    unmounted: el => {
        document.removeEventListener("touchstart", el.eventClearDrag);
        document.removeEventListener("touchmove", el.eventSetDrag);
        document.removeEventListener("click", el.eventOnClick);
        document.removeEventListener("touchend", el.eventOnClick);
        el.removeAttribute("data-dragging");
    },
};
Run Code Online (Sandbox Code Playgroud)

旧答案:

指示

export default {
    beforeMount: (el, binding) => {
        el.eventSetDrag = () => {
            el.setAttribute("data-dragging", "yes");
        };
        el.eventClearDrag = () => {
            el.removeAttribute("data-dragging");
        };
        el.eventOnClick = event => {
            const dragging = el.getAttribute("data-dragging");
            // Check that the click was outside the el and its children, and wasn't a drag
            console.log(document.elementsFromPoint(event.clientX, event.clientY))
            if (!document.elementsFromPoint(event.clientX, event.clientY).includes(el) && !dragging) {
                // call method provided in attribute value
                binding.value(event);
            }
        };
        document.addEventListener("touchstart", el.eventClearDrag);
        document.addEventListener("touchmove", el.eventSetDrag);
        document.addEventListener("click", el.eventOnClick);
        document.addEventListener("touchend", el.eventOnClick);
    },
    unmounted: el => {
        document.removeEventListener("touchstart", el.eventClearDrag);
        document.removeEventListener("touchmove", el.eventSetDrag);
        document.removeEventListener("click", el.eventOnClick);
        document.removeEventListener("touchend", el.eventOnClick);
        el.removeAttribute("data-dragging");
    },
};
Run Code Online (Sandbox Code Playgroud)

此解决方案监视应用指令的组件的元素和元素的子元素,以检查event.target元素是否也是子元素。如果是这种情况,它将不会触发,因为它在组件内部。

如何使用它

您只需使用任何指令,并使用方法引用来处理触发器:

<template>
    <div v-click-outside="myMethod">
        <div class="handle" @click="doAnotherThing($event)">
            <div>Any content</div>
        </div>
    </div>
</template>
Run Code Online (Sandbox Code Playgroud)