Rileva clic all'esterno dell'elemento


120

Come posso rilevare un clic al di fuori del mio elemento? Sto usando Vue.js quindi sarà al di fuori del mio elemento dei modelli. So come farlo in Vanilla JS, ma non sono sicuro che esista un modo più corretto per farlo, quando utilizzo Vue.js?

Questa è la soluzione per Vanilla JS: Javascript Detect Click event outside of div

Immagino di poter utilizzare un modo migliore per accedere all'elemento?


I componenti di Vue sono isolati. quindi rilevare i cambiamenti esterni è fuori questione e viene utilizzato l'anti pattern.
Raj Kamal

Grazie. Non sono sicuro di come implementarlo in un componente Vue. Devono ancora esserci delle buone pratiche per l'anti-pattern?

I componenti Vue.js sono isolati, questo è vero, ma ci sono diversi metodi per la comunicazione genitore-figlio. Quindi, invece di chiedere di rilevare un evento al di fuori di un elemento, dovresti specificare se desideri rilevare elementi all'interno di un componente, dal componente genitore, da qualche figlio, o qualunque relazione sia tra i componenti
Yerko Palma

Grazie per il feedback. Hai alcuni esempi o link a cui posso dare un seguito?

github.com/simplesmiler/vue-clickaway può semplificarti il ​​lavoro
Raj Kamal

Risposte:


96

Può essere risolto bene impostando una direttiva personalizzata una volta:

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

Uso:

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

Nel componente:

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

Demo funzionante su JSFiddle con informazioni aggiuntive sugli avvertimenti:

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


3
Ho usato il vue clickaway, ma penso che la tua soluzione sia più o meno la stessa. Grazie.

56
Questo approccio non funziona più in Vue.js 2. La chiamata self.vm. $ emit dà un messaggio di errore.
Northernman

3
Anche l'uso di @blur è un'opzione e rende molto più semplice dare lo stesso risultato: <input @ blur = "hide"> dove hide: function () {this.isActive = false; }
Scansione

1
La risposta dovrebbe essere modificata per affermare che è solo per Vue.js 1
Stéphane Gerber

167

C'è la soluzione che ho usato, che si basa sulla risposta di Linus Borg e funziona bene con 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)
  },
});

Ti leghi ad esso usando v-click-outside:

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

Ecco una piccola demo

Puoi trovare ulteriori informazioni sulle direttive personalizzate e sul significato di el, binding, vnode in https://vuejs.org/v2/guide/custom-directive.html#Directive-Hook-Arguments


8
Funzionava, ma in Vue 2.0 le direttive non hanno più un'istanza, quindi non è definito. vuejs.org/v2/guide/migration.html#Custom-Directives-simplified . Non ho idea del motivo per cui questo violino funziona o quando questa semplificazione è stata fatta. (Per risolvere, sostituire "questo" con "el" per legare l'evento all'elemento)
Busata

1
Probabilmente funziona perché la finestra è passata come "questo". Ho corretto la risposta. Grazie per aver segnalato questo bug.
MadisonTrash

8
C'è un modo per escludere un elemento specifico all'esterno? Ad esempio, ho un pulsante all'esterno che deve aprire questo elemento e poiché attiva entrambi i metodi non accade nulla.
Žilvinas

5
Puoi spiegare vnode.context [binding.expression] (evento); ?
Sainath SR

1
come cambiarlo in modo che sia possibile utilizzare un'espressione al posto di un metodo all'interno del v-click-outside viene attivato?
raphadko

50

Aggiungi l' tabindexattributo al tuo componente in modo che possa essere focalizzato e procedi come segue:

<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
Whoa! Trovo che questa sia la soluzione più breve e più pulita. Anche l'unico che ha funzionato nel mio caso.
Matt Komarnicki

3
Solo per aggiungere a questo, l'impostazione di un tabindex di -1 interromperà la visualizzazione della casella di evidenziazione quando fai clic sull'elemento, ma consentirà comunque al div di essere focalizzabile.
Colin

1
Per qualche motivo tabindex di -1 non nasconde il contorno per me, quindi ho solo aggiunto outline: none;il focus per l'elemento.
Art3mix

1
come possiamo applicarlo a un navigatore laterale fuori tela che scorre sullo schermo? Non sono in grado di dare il focus sidenav a meno che non venga cliccato,
Charles Okwuagwu

1
Questo è il modo più potente in assoluto. Grazie! :)
Canet Robern

23

Ci sono due pacchetti disponibili nella comunità per questa attività (entrambi sono mantenuti):


8
vue-clickawayil pacchetto ha risolto perfettamente il mio problema. Grazie
Abdalla Arbab

1
Che ne dici di molti articoli? Ogni elemento con un evento clic esterno attiverà l'evento a ogni clic. È bello quando crei dialoghi e terribile quando crei una galleria. Nell'era senza componenti stiamo ascoltando il clic dal documento e controlliamo quale elemento è stato cliccato. Ma ora è un dolore.
br.

@Julien Le Coupanec Ho trovato questa soluzione di gran lunga la migliore! Grazie mille per averlo condiviso!
Manuel Abascal

7

Questo ha funzionato per me con 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
  }
}

Grazie per questo esempio. Controllato su vue 2.6. C'è qualche correzione, nel metodo unbind devi risolvere qualche problema con questo (hai dimenticato la proprietà body nel metodo unbind): document.body.removeEventListener ('click', el .__ vueClickOutside__); in caso contrario, causerà la creazione di più listener di eventi dopo ogni ricreazione del componente (aggiornamento della pagina);
Alexey Shabramov

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

  }
}

5

Ho combinato tutte le risposte (inclusa una riga da vue-clickaway) e ho trovato questa soluzione che funziona per me:

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

Utilizzare nel componente:

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

Più o meno la stessa risposta di @MadisonTrash di seguito
retrovertigo

3

Ho aggiornato la risposta di MadisonTrash per supportare Mobile Safari (che non ha clickeventi, touchenddeve essere usato invece). Questo include anche un controllo in modo che l'evento non venga attivato trascinando sui dispositivi mobili.

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

3

Uso questo codice:

pulsante mostra-nascondi

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

elemento mostra-nascondi

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

copione

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

Aggiorna: rimuovi l'orologio; aggiungi stop propagation


2

Odio le funzioni aggiuntive quindi ... ecco una fantastica soluzione vue senza metodi vue aggiuntivi, solo var

  1. creare elemento html, impostare controlli e direttive
    <p @click="popup = !popup" v-out="popup">

    <div v-if="popup">
       My awesome popup
    </div>
  1. creare una variabile nei dati come
data:{
   popup: false,
}
  1. aggiungere la direttiva vue. suo
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

Se stai cercando specificamente un clic all'esterno dell'elemento ma ancora all'interno del genitore, puoi utilizzare

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

Lo uso per i modali.


1

Puoi registrare due listener di eventi per un evento clic come questo

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

Grazie. Lo so, ma sembra che ci debba essere un modo migliore per farlo in Vue.js?

OK! lascia che qualche genio di vue.js risponda :)
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;
      }
    },
  },

Grazie, funziona perfettamente e se ne hai bisogno solo una volta non c'è bisogno di biblioteche extra
Marian Klühspies

1

La risposta breve: questo dovrebbe essere fatto con le direttive personalizzate .

Ci sono molte ottime risposte qui che dicono anche questo, ma la maggior parte delle risposte che ho visto si interrompono quando inizi a utilizzare ampiamente il clic esterno (specialmente a strati o con più esclusioni). Ho scritto un articolo su supporto parlando delle sfumature delle direttive doganali e in particolare dell'attuazione di questa. Potrebbe non coprire tutti i casi limite, ma ha coperto tutto ciò che ho pensato.

Ciò terrà conto di più associazioni, più livelli di esclusioni di altri elementi e consentirà al gestore di gestire solo la "logica aziendale".

Ecco il codice per almeno la parte di definizione di esso, controlla l'articolo per una spiegazione completa.

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

L'ho fatto in un modo leggermente diverso usando una funzione all'interno di created ().

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

In questo modo, se qualcuno fa clic al di fuori dell'elemento, nel mio caso il navigatore mobile è nascosto.

Spero che questo ti aiuti!


1

Ci sono già molte risposte a questa domanda e la maggior parte di esse si basa sull'idea di direttiva personalizzata simile. Il problema con questo approccio è che si deve passare una funzione di metodo alla direttiva e non è possibile scrivere codice direttamente come in altri eventi.

Ho creato un nuovo pacchetto vue-on-clickoutche è diverso. Dai un'occhiata a:

Permette di scrivere v-on:clickoutproprio come qualsiasi altro evento. Ad esempio, puoi scrivere

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

e funziona.

Aggiornare

vue-on-clickout ora supporta Vue 3!


0

Solo se qualcuno sta cercando come nascondere il modale quando fa clic al di fuori del modale. Poiché modal di solito ha il suo wrapper con class of modal-wrapo qualsiasi cosa tu abbia chiamato, puoi metterlo @click="closeModal"sul wrapper. Utilizzo della gestione degli eventi indicata nella documentazione di vuejs, è possibile verificare se l'obiettivo su cui si è fatto clic si trova sul wrapper o sul modale.

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

Le soluzioni @Denis Danilenko funzionano per me, ecco cosa ho fatto: A proposito, sto usando VueJS CLI3 e NuxtJS qui e con Bootstrap4, ma funzionerà anche su VueJS senza 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;
    }
  }
}

0

È possibile emettere un evento javascript nativo personalizzato da una direttiva. Crea una direttiva che invia un evento dal nodo, utilizzando 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)
    }
})

Che può essere usato in questo modo

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

0

Creo un div alla fine del corpo in questo modo:

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

Dove .outside è:

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

E away () è un metodo nell'istanza di Vue:

away() {
 this.isPopup = false;
}

Facile, funziona bene.


0

Se hai un componente con più elementi all'interno dell'elemento radice puoi usare questa soluzione It just works ™ con un valore booleano.

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

0

Usa questo pacchetto vue-click-outside

È semplice e affidabile, attualmente utilizzato da molti altri pacchetti. Puoi anche ridurre la dimensione del pacchetto javascript chiamando il pacchetto solo nei componenti richiesti (vedi esempio sotto).

npm install vue-click-outside

Utilizzo:

<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

È possibile creare un nuovo componente che gestisce il clic esterno

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

E usa questo componente:

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

-1

spesso le persone vogliono sapere se l'utente lascia il componente root (funziona con qualsiasi componente di livello)

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


-1

Ho una soluzione per la gestione del menu a discesa di attivazione / disattivazione:

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

Sto usando questo pacchetto: https://www.npmjs.com/package/vue-click-outside

Funziona bene per me

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>

I miei codici di script:

import ClickOutside from 'vue-click-outside'
export default
{
    data(){
        return {
            cardContentVisible:false
        }
    },
    created()
    {
    },
    methods:
        {
            openCardContent()
            {
                this.cardContentVisible = true;
            }, hide () {
            this.cardContentVisible = false
                }
        },
    directives: {
            ClickOutside
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.