检测单击外部元素

时间:2016-03-23 05:24:55

标签: javascript vue.js

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

这是Vanilla JS的解决方案:Javascript Detect Click event outside of div

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

31 个答案:

答案 0 :(得分:109)

我使用的解决方案基于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)
  },
});

demo

您可以在https://vuejs.org/v2/guide/custom-directive.html#Directive-Hook-Arguments

中找到有关自定义指令以及 el,binding,vnode 的更多信息

答案 1 :(得分:56)

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

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() }
})

<强>用法:

<div v-click-outside="nameOfCustomEventToCall">
  Some content
</div>

在组件中:

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

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

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

答案 2 :(得分:12)

社区中有两个可用于此任务的软件包(两者都已维护):

答案 3 :(得分:6)

向您的组件添加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>

答案 4 :(得分:5)

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

  }
}

答案 5 :(得分:3)

vue 3 的完整案例

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

编辑:

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

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");
    },
};

旧答案:

指令

const clickOutside = {
    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
            if (!(el == event.target || el.contains(event.target)) && !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");
    },
}

createApp(App)
  .directive("click-outside", clickOutside)
  .mount("#app");

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

如何使用

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

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

答案 6 :(得分:3)

这适用于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
  }
}

答案 7 :(得分:2)

我使用此代码:

显示隐藏按钮

 <a @click.stop="visualSwitch()"> show hide </a>

show-hide元素

<div class="dialog-popup" v-if="visualState" @click.stop=""></div>

脚本

data () { return {
    visualState: false,
}},
methods: {
    visualSwitch() {
        this.visualState = !this.visualState;
        if (this.visualState)
            document.addEventListener('click', this.visualState);
        else
            document.removeEventListener('click', this.visualState);
    },
},

更新:删除观看;添加停止传播

答案 8 :(得分:2)

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

<div class="parent" @click.self="onParentClick">
  <div class="child"></div>
</div>

我将其用于模态

答案 9 :(得分:1)

对于Vue 3:

此答案基于MadisonTrash的great answer above,但已更新为使用新的Vue 3语法。

Vue 3现在使用beforeMount代替bind,并且使用unmounted代替unbindsrc)。

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.body.addEventListener("click", el.clickOutsideEvent);
  },
  unmounted: el => {
    document.body.removeEventListener("click", el.clickOutsideEvent);
  },
};

createApp(App)
  .directive("click-outside", clickOutside)
  .mount("#app");

答案 10 :(得分:1)

我讨厌其他功能,所以...这是一个很棒的vue解决方案,没有其他vue方法,只有var

  1. 创建html元素,设置控件和指令
    <p @click="popup = !popup" v-out="popup">

    <div v-if="popup">
       My awesome popup
    </div>
  1. 在像这样的数据中创建var
data:{
   popup: false,
}
  1. 添加vue指令。其
Vue.directive('out', {

    bind: function (el, binding, vNode) {
        const handler = (e) => {
            if (!el.contains(e.target) && el !== e.target) {
                //and here is you toggle var. thats it
                vNode.context[binding.expression] = false
            }
        }
        el.out = handler
        document.addEventListener('click', handler)
    },

    unbind: function (el, binding) {
        document.removeEventListener('click', el.out)
        el.out = null
    }
})

答案 11 :(得分:1)

简短的答案:应该使用Custom Directives完成。

这里有很多很棒的答案,也可以这么说,但是当您开始广泛使用外部单击(尤其是分层或多个排除项)时,我看到的大多数答案都无法使用。我在介质上写了article,讨论了自定义指令的细微差别,尤其是该指令的实现。它可能无法涵盖所有​​极端情况,但涵盖了我所想到的所有内容。

这将说明多个绑定,其他元素排除的多个级别,并允许您的处理程序仅管理“业务逻辑”。

这里至少是其中定义部分的代码,请查看文章以获取完整说明。

https://example.com/1234/http%3A%2F%2F5external-link.com
https://example.com/1234/http://external-link.com

答案 12 :(得分:1)

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

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

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

希望这会有所帮助!

答案 13 :(得分:1)

Vue 3的指令有重大更改,所有Vue 3中进行操作,请参见以下代码段。有关信息,请通过this link

<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)
  }
}

并在main.js中添加以下内容

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

createApp(App)
 .directive('click-outside', ClickOutside)
 .use(IfAnyModules)
 .mount('#app')

答案 14 :(得分:1)

我更新了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');
    },
});

答案 15 :(得分:1)

您可以为这样的点击事件注册两个事件监听器

document.getElementById("some-area")
        .addEventListener("click", function(e){
        alert("You clicked on the area!");
        e.stopPropagation();// this will stop propagation of this event to upper level
     }
);

document.body.addEventListener("click", 
   function(e) {
           alert("You clicked outside the area!");
         }
);

答案 16 :(得分:0)

您可以创建处理外部点击的新组件

Vue.component('click-outside', {
  created: function () {
    document.body.addEventListener('click', (e) => {
       if (!this.$el.contains(e.target)) {
            this.$emit('clickOutside');
           
        })
  },
  template: `
    <template>
        <div>
            <slot/>
        </div>
    </template>
`
})

并使用此组件:

<template>
    <click-outside @clickOutside="console.log('Click outside Worked!')">
      <div> Your code...</div>
    </click-outside>
</template>

答案 17 :(得分:0)

我不确定是否有人会看到这个答案,但它就在这里。 这里的想法是简单地检测是否在元素本身之外进行了任何点击。

我首先为我的“下拉列表”的主 div 提供一个 id。

<template>
  <div class="relative" id="dropdown">
    <div @click="openDropdown = !openDropdown" class="cursor-pointer">
      <slot name="trigger" />
    </div>

    <div
      class="absolute mt-2 w-48 origin-top-right right-0 text-red  bg-tertiary text-sm text-black"
      v-show="openDropdown"
      @click="openDropdown = false"
    >
      <slot name="content" />
    </div>
  </div>
</template>

然后我只是遍历鼠标事件的路径,看看我的 id 为“下拉列表”的 div 是否在那里。如果是,那么我们很好,如果不是,那么我们关闭下拉菜单。

<script>
export default {
  data() {
    return {
      openDropdown: false,
    };
  },
  created() {
    document.addEventListener("click", (e) => {
      let me = false;
      for (let index = 0; index < e.path.length; index++) {
        const element = e.path[index];

        if (element.id == "dropdown") {
          me = true;
          return;
        }
      }

      if (!me) this.openDropdown = false;
    });
  }
};
</script>

如果您有很多嵌套元素,我很确定这会带来性能问题,但我发现这是最懒惰的方法。

答案 18 :(得分:0)

不要重新发明轮子,请使用此软件包v-click-outside

答案 19 :(得分:0)

该问题已经有很多答案,并且大多数答案都基于类似的自定义指令思想。这种方法的问题在于,必须将方法函数传递给指令,并且不能像其他事件一样直接编写代码。

我创建了一个不同的新软件包vue-on-clickout。在以下位置查看:

它可以像其他事件一样写v-on:clickout。例如,您可以编写

<div v-on:clickout="myField=value" v-on:click="myField=otherValue">...</div>

它有效。

答案 20 :(得分:0)

  <button 
    class="dropdown"
    @click.prevent="toggle"
    ref="toggle"
    :class="{'is-active': isActiveEl}"
  >
    Click me
  </button>

  data() {
   return {
     isActiveEl: false
   }
  }, 
  created() {
    window.addEventListener('click', this.close);
  },
  beforeDestroy() {
    window.removeEventListener('click', this.close);
  },
  methods: {
    toggle: function() {
      this.isActiveEl = !this.isActiveEl;
    },
    close(e) {
      if (!this.$refs.toggle.contains(e.target)) {
        this.isActiveEl = false;
      }
    },
  },

答案 21 :(得分:0)

使用此程序包 vue-click-outside

它简单可靠,目前已被许多其他软件包使用。您还可以通过仅在必需的组件中调用包来减小javascript包的大小(请参见下面的示例)。

npm install vue-click-outside

用法:

<template>
  <div>
    <div v-click-outside="hide" @click="toggle">Toggle</div>
    <div v-show="opened">Popup item</div>
  </div>
</template>

<script>
import ClickOutside from 'vue-click-outside'

export default {
  data () {
    return {
      opened: false
    }
  },

  methods: {
    toggle () {
      this.opened = true
    },

    hide () {
      this.opened = false
    }
  },

  mounted () {
    // prevent click outside event with popupItem.
    this.popupItem = this.$el
  },

  // do not forget this section
  directives: {
    ClickOutside
  }
}
</script>

答案 22 :(得分:0)

如果您的组件在根元素内包含多个元素,则可以使用带有布尔值的 It just works™解决方案。

<template>
  <div @click="clickInside"></div>
<template>
<script>
export default {
  name: "MyComponent",
  methods: {
    clickInside() {
      this.inside = true;
      setTimeout(() => (this.inside = false), 0);
    },
    clickOutside() {
      if (this.inside) return;
      // handle outside state from here
    }
  },
  created() {
    this.__handlerRef__ = this.clickOutside.bind(this);
    document.body.addEventListener("click", this.__handlerRef__);
  },
  destroyed() {
    document.body.removeEventListener("click", this.__handlerRef__);
  },
};
</script>

答案 23 :(得分:0)

我在主体的末端创建一个div,如下所示:

<div v-if="isPopup" class="outside" v-on:click="away()"></div>

.outside在哪里:

.outside {
  width: 100vw;
  height: 100vh;
  position: fixed;
  top: 0px;
  left: 0px;
}

away()是Vue实例中的方法:

away() {
 this.isPopup = false;
}

容易,效果很好。

答案 24 :(得分:0)

您可以从指令发出自定义的本地javascript事件。使用node.dispatchEvent

创建一个从节点调度事件的指令。
let handleOutsideClick;
Vue.directive('out-click', {
    bind (el, binding, vnode) {

        handleOutsideClick = (e) => {
            e.stopPropagation()
            const handler = binding.value

            if (el.contains(e.target)) {
                el.dispatchEvent(new Event('out-click')) <-- HERE
            }
        }

        document.addEventListener('click', handleOutsideClick)
        document.addEventListener('touchstart', handleOutsideClick)
    },
    unbind () {
        document.removeEventListener('click', handleOutsideClick)
        document.removeEventListener('touchstart', handleOutsideClick)
    }
})

可以这样使用

h3( v-out-click @click="$emit('show')" @out-click="$emit('hide')" )

答案 25 :(得分:0)

@Denis Danilenko解决方案为我工作,这是我所做的: 顺便说一下,我在这里和Bootstrap4中都使用了VueJS CLI3和NuxtJS,但是它也可以在没有NuxtJS的情况下在VueJS上使用:

<div
    class="dropdown ml-auto"
    :class="showDropdown ? null : 'show'">
    <a 
        href="#" 
        class="nav-link" 
        role="button" 
        id="dropdownMenuLink" 
        data-toggle="dropdown" 
        aria-haspopup="true" 
        aria-expanded="false"
        @click="showDropdown = !showDropdown"
        @blur="unfocused">
        <i class="fas fa-bars"></i>
    </a>
    <div 
        class="dropdown-menu dropdown-menu-right" 
        aria-labelledby="dropdownMenuLink"
        :class="showDropdown ? null : 'show'">
        <nuxt-link class="dropdown-item" to="/contact">Contact</nuxt-link>
        <nuxt-link class="dropdown-item" to="/faq">FAQ</nuxt-link>
    </div>
</div>
export default {
    data() {
        return {
            showDropdown: true
        }
    },
    methods: {
    unfocused() {
        this.showDropdown = !this.showDropdown;
    }
  }
}

答案 26 :(得分:0)

我结合了所有答案(包括来自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() {
 document.body.removeEventListener('click', el.clickOutsideEvent);
} })

在组件中使用:

<li v-click-outside="closeSearch">
  <!-- your component here -->
</li>

答案 27 :(得分:0)

如果有人在模态外部单击时正在寻找如何隐藏模态的方法。由于模式通常具有awk类或您命名的任何类的包装器,因此可以将modal-wrap放在包装器上。使用vuejs文档中所述的event handling,您可以检查单击的目标是在包装器上还是在模式中。

@click="closeModal"
methods: {
  closeModal(e) {
    this.event = function(event) {
      if (event.target.className == 'modal-wrap') {
        // close modal here
        this.$store.commit("catalog/hideModal");
        document.body.removeEventListener("click", this.event);
      }
    }.bind(this);
    document.body.addEventListener("click", this.event);
  },
}

答案 28 :(得分:-1)

我正在使用此软件包:https://www.npmjs.com/package/vue-click-outside

对我来说很好

HTML:

<div class="__card-content" v-click-outside="hide" v-if="cardContentVisible">
    <div class="card-header">
        <input class="subject-input" placeholder="Subject" name=""/>
    </div>
    <div class="card-body">
        <textarea class="conversation-textarea" placeholder="Start a conversation"></textarea>
    </div>
</div>

我的脚本代码:

import ClickOutside from 'vue-click-outside'
export default
{
    data(){
        return {
            cardContentVisible:false
        }
    },
    created()
    {
    },
    methods:
        {
            openCardContent()
            {
                this.cardContentVisible = true;
            }, hide () {
            this.cardContentVisible = false
                }
        },
    directives: {
            ClickOutside
    }
}

答案 29 :(得分:-1)

我有一个处理切换下拉菜单的解决方案:

export default {
data() {
  return {
    dropdownOpen: false,
  }
},
methods: {
      showDropdown() {
        console.log('clicked...')
        this.dropdownOpen = !this.dropdownOpen
        // this will control show or hide the menu
        $(document).one('click.status', (e)=> {
          this.dropdownOpen = false
        })
      },
}

答案 30 :(得分:-1)

人们常常想知道用户是否留下根组件(适用于任何级别组件)

&#13;
&#13;
Vue({
  data: {},
  methods: {
    unfocused : function() {
      alert('good bye');
    }
  }
})
&#13;
<template>
  <div tabindex="1" @blur="unfocused">Content inside</div>
</template>
&#13;
&#13;
&#13;