Обнаружить щелчок за пределами элемента

122

Как я могу обнаружить щелчок за пределами моего элемента? Я использую Vue.js, поэтому он будет вне моего элемента шаблонов. Я знаю, как это сделать в Vanilla JS, но не уверен, есть ли более правильный способ сделать это, когда я использую Vue.js?

Это решение для Vanilla JS: событие Javascript Detect Click за пределами div

Думаю, я могу использовать лучший способ получить доступ к элементу?

Сообщество
источник
Компоненты Vue изолированы. поэтому об обнаружении внешних изменений не может быть и речи, и используется антипаттерн.
Радж Камаль
Спасибо. Однако я не уверен, как реализовать это в компоненте Vue. Должны же быть какие-то лучшие практики для анти-паттерна?
Компонент Vue.js изолирован, это правда, но есть разные методы для общения родитель-потомок. Таким образом, вместо того, чтобы запрашивать обнаружение события вне элемента, вы должны указать, хотите ли вы обнаруживать элементы внутри компонента, из родительского компонента, из некоторого дочернего компонента или любого другого отношения между компонентами
Йерко Пальма,
Спасибо за ответ. У вас есть примеры или ссылки, по которым я могу следить?
github.com/simplesmiler/vue-clickaway может упростить вашу работу
Радж Камаль,

Ответы:

98

Можно хорошо решить, настроив настраиваемую директиву один раз:

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/

Линус Борг
источник
3
Я использовал vue clickaway, но думаю, что ваше решение более или менее похоже. Спасибо.
56
Этот подход больше не работает в Vue.js 2. Вызов self.vm. $ emit выдает сообщение об ошибке.
northernman
3
Использование @blur также является вариантом и упрощает получение того же результата: <input @ blur = "hide"> where hide: function () {this.isActive = false; }
Craws
1
Ответ должен быть отредактирован, чтобы указать, что он предназначен только для Vue.js 1
Стефан Гербер,
169

Я использовал решение, основанное на ответе Линуса Борга и отлично работающее с 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 children
      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)
  },
});

Вы привязываетесь к нему, используя v-click-outside:

<div v-click-outside="doStuff">

Вот небольшая демонстрация

Вы можете найти дополнительную информацию о пользовательских директивах и о том , что означает el, binding, vnode, в https://vuejs.org/v2/guide/custom-directive.html#Directive-Hook-Arguments

MadisonTrash
источник
8
Работает, но в директивах Vue 2.0 больше нет экземпляра, поэтому он не определен. vuejs.org/v2/guide/migration.html#Custom-Directives-simplified . Я понятия не имею, почему эта скрипка работает или когда это упрощение было сделано. (Чтобы решить эту проблему, замените «this» на «el», чтобы привязать событие к элементу)
Busata
1
Это работает, вероятно, потому, что окно прошло как «это». Я исправил ответ. Спасибо, что указали на эту ошибку.
MadisonTrash
8
Есть ли способ исключить определенный элемент снаружи? Например, у меня есть одна кнопка снаружи, которая должна открывать этот элемент, и поскольку она запускает оба метода, ничего не происходит.
ilvinas
5
Не могли бы вы объяснить vnode.context [binding.expression] (событие); ?
Sainath SR
1
как изменить это так, чтобы выражение могло использоваться вместо метода внутри v-click-outside срабатывает?
raphadko
50

Добавьте 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
Вау! Я считаю это самым коротким и чистым решением. Также единственный, который работал в моем случае.
Мэтт Комарницки
3
Чтобы добавить к этому, установка tabindex на -1 остановит отображение поля выделения, когда вы щелкнете по элементу, но по-прежнему позволит фокусировать div.
Колин
1
По какой-то причине tabindex, равный -1, не скрывает для меня контур, поэтому я просто добавил outline: none;фокус для элемента.
Art3mix 06
1
как мы можем применить это к боковой панели навигации вне холста, которая скользит по экрану? Я не могу сфокусировать боковую навигацию, если по ней не щелкнуть,
Чарльз Оквуагву
1
Это абсолютно самый мощный способ. Спасибо! :)
Canet Robern
23

В сообществе доступны два пакета для этой задачи (оба поддерживаются):

Жюльен Ле Купанек
источник
8
vue-clickawayпакет отлично решил мою проблему. Спасибо
Абдалла Арбаб
1
А как насчет множества предметов? Каждый элемент с внешним событием щелчка будет запускать событие при каждом щелчке. Приятно, когда вы делаете диалоги и ужасно, когда создаете галерею. В эпоху без компонентов мы слушаем щелчок из документа и проверяем, какой элемент был нажат. Но теперь это боль.
br.
@Julien Le Coupanec Я нашел это решение лучшим на сегодняшний день! Большое спасибо, что поделились этим!
Мануэль Абаскаль
7

Это сработало для меня с 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
  }
}
yann_yinn
источник
Спасибо за этот пример. Проверял это на vue 2.6. Есть некоторое исправление, в методе unbind вы должны исправить некоторую проблему этим (вы забыли свойство body в методе unbind): document.body.removeEventListener ('click', el .__ vueClickOutside__); если нет - это вызовет создание нескольких слушателей событий после каждого воссоздания компонента (обновления страницы);
Алексей Шабрамов
7
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

  }
}
xiaoyu2er
источник
5

Я объединил все ответы (включая строку из 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);
    }
});

Использование в компоненте:

<li v-click-outside="closeSearch">
  <!-- your component here -->
</li>
BogdanG
источник
Практически такой же , как @MadisonTrash ответ ниже
retrovertigo
3

Я обновил ответ 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');
    },
});
benrwb
источник
3

Я использую этот код:

кнопка показать-скрыть

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

показать-скрыть элемент

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

Обновление: удалить часы; добавить остановку распространения

Pax Exterminatus
источник
2

Я ненавижу дополнительные функции, поэтому ... вот отличное решение vue без дополнительных методов vue, только var

  1. создать элемент html, установить элементы управления и директиву
    <p @click="popup = !popup" v-out="popup">

    <div v-if="popup">
       My awesome popup
    </div>
  1. создать переменную в данных вроде
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
    }
})
Мартин Престон
источник
2

Если вы специально ищете щелчок за пределами элемента, но все еще внутри родительского элемента, вы можете использовать

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

Я использую это для модальных окон.

Андрес Ольгин
источник
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!");
         }
);
saravanakumar
источник
Спасибо. Я знаю это, но кажется, что должен быть лучший способ сделать это в Vue.js?
ХОРОШО! пусть ответит какой-нибудь гений vue.js :)
saravanakumar
1
  <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;
      }
    },
  },
Дмитрий Лиштван
источник
Спасибо, работает отлично, и если вам это нужно только один раз, дополнительные библиотеки не нужны
Мариан Клюхспис,
1

Краткий ответ: это нужно делать с помощью специальных директив .

Здесь есть много отличных ответов, которые также говорят об этом, но большинство ответов, которые я видел, ломаются, когда вы начинаете широко использовать внешний щелчок (особенно многоуровневый или с несколькими исключениями). Я написал статью о среде, в которой рассказывается о нюансах пользовательских директив и, в частности, о реализации этой. Возможно, он не охватывает все крайние случаи, но он охватил все, что я придумал.

Это будет учитывать несколько привязок, несколько уровней исключений других элементов и позволит вашему обработчику управлять только «бизнес-логикой».

Вот код, по крайней мере, для его части определения, ознакомьтесь со статьей для полного объяснения.

var handleOutsideClick={}
const OutsideClick = {
  // this directive is run on the bind and unbind hooks
  bind (el, binding, vnode) {
    // Define the function to be called on click, filter the excludes and call the handler
    handleOutsideClick[el.id] = e => {
      e.stopPropagation()
      // extract the handler and exclude from the binding value
      const { handler, exclude } = binding.value
      // set variable to keep track of if the clicked element is in the exclude list
      let clickedOnExcludedEl = false
      // if the target element has no classes, it won't be in the exclude list skip the check
      if (e.target._prevClass !== undefined) {
        // for each exclude name check if it matches any of the target element's classes
        for (const className of exclude) {
          clickedOnExcludedEl = e.target._prevClass.includes(className)
          if (clickedOnExcludedEl) {
            break // once we have found one match, stop looking
          }
        }
      }
      // don't call the handler if our directive element contains the target element
      // or if the element was in the exclude list
      if (!(el.contains(e.target) || clickedOnExcludedEl)) {
        handler()
      }
    }
    // Register our outsideClick handler on the click/touchstart listeners
    document.addEventListener('click', handleOutsideClick[el.id])
    document.addEventListener('touchstart', handleOutsideClick[el.id])
    document.onkeydown = e => {
      //this is an option but may not work right with multiple handlers
      if (e.keyCode === 27) {
        // TODO: there are minor issues when escape is clicked right after open keeping the old target
        handleOutsideClick[el.id](e)
      }
    }
  },
  unbind () {
    // If the element that has v-outside-click is removed, unbind it from listeners
    document.removeEventListener('click', handleOutsideClick[el.id])
    document.removeEventListener('touchstart', handleOutsideClick[el.id])
    document.onkeydown = null //Note that this may not work with multiple listeners
  }
}
export default OutsideClick
Маркус Смит
источник
1

Я сделал это немного по-другому, используя функцию в created ().

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

Таким образом, если кто-то щелкает за пределами элемента, то в моем случае мобильная навигация скрывается.

Надеюсь это поможет!

Почти Питт
источник
1

На этот вопрос уже есть много ответов, и большинство из них основаны на аналогичной идее настраиваемой директивы. Проблема с этим подходом заключается в том, что нужно передать функцию метода в директиву и нельзя напрямую писать код, как в других событиях.

Я создал новый пакет, vue-on-clickoutкоторый отличается. Проверьте это на:

Это позволяет писать v-on:clickout же, как и любые другие события. Например, вы можете написать

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

и это работает.

Обновить

vue-on-clickout теперь поддерживает Vue 3!

Му-Цун Цай
источник
0

Просто если кто-нибудь смотрит как скрыть модальное окно при нажатии вне модального окна. Поскольку у модального окна обычно есть обертка с классом modal-wrapили любым другим именем, вы можете установить @click="closeModal"оболочку. Используя обработку событий, указанную в документации vuejs, вы можете проверить, находится ли выбранная цель либо на оболочке, либо в модальном окне.

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);
  },
}
<div class="modal-wrap" @click="closeModal">
  <div class="modal">
    ...
  </div>
<div>

джедай
источник
0

Решения @Denis Danilenko работают для меня, вот что я сделал: Кстати, я использую VueJS CLI3 и NuxtJS здесь и с Bootstrap4, но он также будет работать на VueJS без NuxtJS:

<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;
    }
  }
}
alfieindesigns
источник
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')" )
Педро Торкио
источник
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;
}

Легко, работает хорошо.

Арно Лидз
источник
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>
A1rPun
источник
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>
Смит Патель
источник
0

Не изобретайте велосипед, используйте этот пакет v-click-outside

snehanshu.js
источник
Ознакомьтесь с моим ответом, который, я подозреваю, вам понравится больше.
Му-Цун Цай,
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>
Диктатор47
источник
-1

часто люди хотят знать, покидает ли пользователь корневой компонент (работает с компонентами любого уровня)

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

Денис Даниленко
источник
-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
        })
      },
}
Николя С.Сю
источник
-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
    }
}
Мурад Шукурлу
источник