Jak wykryć kliknięcie poza elementem?

2486

Mam kilka menu HTML, które wyświetlam całkowicie, gdy użytkownik kliknie ich górną część. Chciałbym ukryć te elementy, gdy użytkownik kliknie poza obszarem menu.

Czy coś takiego jest możliwe w jQuery?

$("#menuscontainer").clickOutsideThisElement(function() {
    // Hide the menus
});
Sergio del Amo
źródło
47
Oto przykład tej strategii: jsfiddle.net/tedp/aL7Xe/1
Ted
18
Jak wspomniał Tom, przed użyciem tego podejścia należy przeczytać css-tricks.com/dangers-stopping-event-propagation . To narzędzie jsfiddle jest jednak całkiem fajne.
Jon Coombs
3
uzyskaj odniesienie do elementu, a następnie event.target, a na koniec! = lub == oba z nich, a następnie odpowiednio wykonaj kod.
Rohit Kumar
Polecam użyć github.com/gsantiago/jquery-clickout :)
Rômulo M. Farias
2
Rozwiązanie wanilia JS z event.targeti bez event.stopPropagation .
lowtechsun

Odpowiedzi:

1812

UWAGA: Używanie stopEventPropagation()jest czymś, czego należy unikać, ponieważ przerywa normalny przepływ zdarzeń w DOM. Zobacz ten artykuł, aby uzyskać więcej informacji. Zamiast tego rozważ użycie tej metody

Dołącz zdarzenie click do treści dokumentu, która zamyka okno. Dołącz osobne zdarzenie kliknięcia do kontenera, które zatrzymuje propagację do treści dokumentu.

$(window).click(function() {
//Hide the menus if visible
});

$('#menucontainer').click(function(event){
    event.stopPropagation();
});
Eran Galperin
źródło
708
To łamie standardowe zachowanie wielu rzeczy, w tym przycisków i linków, zawartych w #menucontainer. Dziwi mnie, że ta odpowiedź jest tak popularna.
Art.
75
Nie psuje to zachowania czegokolwiek wewnątrz #menucontainer, ponieważ znajduje się on na dole łańcucha propagacji czegokolwiek w nim.
Eran Galperin
94
jest bardzo piękna, ale nie powinieneś używać $('html').click()ciała. Ciało zawsze ma wysokość swojej zawartości. Nie ma dużo treści lub ekran jest bardzo wysoki, działa tylko na części wypełnionej ciałem.
meo
103
Jestem również zaskoczony, że to rozwiązanie uzyskało tak wiele głosów. Nie powiedzie się to dla żadnego elementu poza stopPropagation jsfiddle.net/Flandre/vaNFw/3
Andre
140
Philip Walton bardzo dobrze wyjaśnia, dlaczego ta odpowiedź nie jest najlepszym rozwiązaniem: css-tricks.com/dangers-stopping-event-propagation
Tom
1386

Możesz nasłuchiwać zdarzenia kliknięcia , documenta następnie upewnić się, że #menucontainernie jest on przodkiem ani celem klikniętego elementu, używając .closest().

Jeśli tak nie jest, kliknięty element znajduje się na zewnątrz #menucontaineri można go bezpiecznie ukryć.

$(document).click(function(event) { 
  $target = $(event.target);
  if(!$target.closest('#menucontainer').length && 
  $('#menucontainer').is(":visible")) {
    $('#menucontainer').hide();
  }        
});

Edycja - 23.06.2017

Możesz także posprzątać po detektorze zdarzeń, jeśli planujesz zamknąć menu i chcesz przestać nasłuchiwać zdarzeń. Ta funkcja wyczyści tylko nowo utworzony detektor, zachowując wszystkie inne detektory kliknięć document. Ze składnią ES2015:

export function hideOnClickOutside(selector) {
  const outsideClickListener = (event) => {
    $target = $(event.target);
    if (!$target.closest(selector).length && $(selector).is(':visible')) {
        $(selector).hide();
        removeClickListener();
    }
  }

  const removeClickListener = () => {
    document.removeEventListener('click', outsideClickListener)
  }

  document.addEventListener('click', outsideClickListener)
}

Edytuj - 2018-03-11

Dla tych, którzy nie chcą korzystać z jQuery. Oto powyższy kod w postaci zwykłego vanillaJS (ECMAScript6).

function hideOnClickOutside(element) {
    const outsideClickListener = event => {
        if (!element.contains(event.target) && isVisible(element)) { // or use: event.target.closest(selector) === null
          element.style.display = 'none'
          removeClickListener()
        }
    }

    const removeClickListener = () => {
        document.removeEventListener('click', outsideClickListener)
    }

    document.addEventListener('click', outsideClickListener)
}

const isVisible = elem => !!elem && !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ) // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js 

UWAGA: Jest to oparte na komentarzu Alexa, którego należy użyć !element.contains(event.target)zamiast części jQuery.

Ale element.closest()jest teraz dostępny również we wszystkich głównych przeglądarkach (wersja W3C różni się nieco od wersji jQuery). Polyfills można znaleźć tutaj: Element.closest ()

Edytuj - 2020-05-21

W przypadku, gdy chcesz, aby użytkownik mógł klikać i przeciągać wewnątrz elementu, a następnie zwolnij mysz poza elementem, nie zamykając elementu:

      ...
      let lastMouseDownX = 0;
      let lastMouseDownY = 0;
      let lastMouseDownWasOutside = false;

      const mouseDownListener = (event: MouseEvent) => {
        lastMouseDownX = event.offsetX
        lastMouseDownY = event.offsetY
        lastMouseDownWasOutside = !$(event.target).closest(element).length
      }
      document.addEventListener('mousedown', mouseDownListener);

I w outsideClickListener:

const outsideClickListener = event => {
        const deltaX = event.offsetX - lastMouseDownX
        const deltaY = event.offsetY - lastMouseDownY
        const distSq = (deltaX * deltaX) + (deltaY * deltaY)
        const isDrag = distSq > 3
        const isDragException = isDrag && !lastMouseDownWasOutside

        if (!element.contains(event.target) && isVisible(element) && !isDragException) { // or use: event.target.closest(selector) === null
          element.style.display = 'none'
          removeClickListener()
          document.removeEventListener('mousedown', mouseDownListener); // Or add this line to removeClickListener()
        }
    }
Art
źródło
28
Próbowałem wielu innych odpowiedzi, ale tylko ta zadziałała. Dzięki. Kod, który ostatecznie wykorzystałem to: $ (dokument). Kliknięcie (funkcja (zdarzenie) {if ($ (event.target) .closest ('. Window'). Length == 0) {$ ('. Window' ) .fadeOut ('fast');}});
Pistos
39
Właściwie skończyłem z tym rozwiązaniem, ponieważ lepiej obsługuje wiele menu na tej samej stronie, gdzie kliknięcie drugiego menu, gdy pierwsze jest otwarte, pozostawi pierwsze otwarte w rozwiązaniu stopPropagation.
umassthrower
13
Doskonała odpowiedź. Jest to odpowiedni sposób, gdy masz wiele przedmiotów, które chcesz zamknąć.
Jan
5
To powinna być zaakceptowana odpowiedź z powodu wady drugiego rozwiązania z event.stopPropagation ().
pomidor
20
Bez jQuery - !element.contains(event.target)za pomocą Node.contains ()
Alex Ross
303

Jak wykryć kliknięcie poza elementem?

To pytanie jest tak popularne i zawiera tak wiele odpowiedzi, że jest zwodniczo złożone. Po prawie ośmiu latach i dziesiątkach odpowiedzi jestem naprawdę zaskoczony, widząc, jak mało uwagi poświęcono dostępności.

Chciałbym ukryć te elementy, gdy użytkownik kliknie poza obszarem menu.

To szlachetna przyczyna i faktyczny problem. Tytuł pytania - na które większość odpowiedzi wydaje się próbować odpowiedzieć - zawiera niefortunny czerwony śledź.

Wskazówka: to słowo „kliknij” !

W rzeczywistości nie chcesz wiązać programów obsługi kliknięć.

Jeśli wiążesz procedury obsługi kliknięć w celu zamknięcia okna, już nie powiodło się. Powodem niepowodzenia jest to, że nie wszyscy wyzwalają clickzdarzenia. Użytkownicy nieużywający myszy będą mogli wyjść z twojego okna dialogowego (a twoje menu podręczne jest prawdopodobnie rodzajem okna dialogowego) przez naciśnięcie Tab, a następnie nie będą mogli odczytać treści za oknem dialogowym bez wywołania clickzdarzenia.

Przeformułujmy więc pytanie.

Jak zamknąć okno dialogowe, gdy użytkownik go zakończy?

To jest cel. Niestety, teraz musimy powiązać userisfinishedwiththedialogwydarzenie, a wiązanie to nie jest takie proste.

Jak więc wykryć, że użytkownik zakończył korzystanie z okna dialogowego?

focusout zdarzenie

Dobrym początkiem jest ustalenie, czy fokus opuścił okno dialogowe.

Wskazówka: bądź ostrożny ze blurzdarzeniem, blurnie rozprzestrzenia się, jeśli wydarzenie było związane z fazą bulgotania!

jQuery's focusoutbędzie w porządku. Jeśli nie możesz użyć jQuery, możesz użyć blurpodczas fazy przechwytywania:

element.addEventListener('blur', ..., true);
//                       use capture: ^^^^

Ponadto w wielu oknach dialogowych musisz pozwolić na skupienie się kontenera. Dodaj, tabindex="-1"aby okno dialogowe mogło dynamicznie otrzymywać fokus, nie zakłócając w inny sposób operacji tabulacji.

$('a').on('click', function () {
  $(this.hash).toggleClass('active').focus();
});

$('div').on('focusout', function () {
  $(this).removeClass('active');
});
div {
  display: none;
}
.active {
  display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<a href="#example">Example</a>
<div id="example" tabindex="-1">
  Lorem ipsum <a href="http://example.com">dolor</a> sit amet.
</div>


Jeśli grasz w tę wersję demo przez ponad minutę, powinieneś szybko zacząć widzieć problemy.

Po pierwsze, link w oknie dialogowym nie jest klikalny. Próba kliknięcia na nią lub karty spowoduje zamknięcie okna dialogowego przed interakcją. Wynika to z tego, że skupienie elementu wewnętrznego wyzwala focusoutzdarzenie przed ponownym wywołaniem focusinzdarzenia.

Rozwiązaniem jest kolejkowanie zmiany stanu w pętli zdarzeń. Można to zrobić za pomocą setImmediate(...)lub setTimeout(..., 0)w przeglądarkach, które nie obsługują setImmediate. Po umieszczeniu w kolejce można go anulować poprzez focusin:

$('.submenu').on({
  focusout: function (e) {
    $(this).data('submenuTimer', setTimeout(function () {
      $(this).removeClass('submenu--active');
    }.bind(this), 0));
  },
  focusin: function (e) {
    clearTimeout($(this).data('submenuTimer'));
  }
});

Drugi problem polega na tym, że okno dialogowe nie zamyka się po ponownym naciśnięciu łącza. Wynika to z faktu, że okno dialogowe traci fokus, wyzwalając zachowanie zamknięcia, po czym kliknięcie łącza powoduje ponowne otwarcie okna dialogowego.

Podobnie jak w poprzednim wydaniu, stanem skupienia należy zarządzać. Biorąc pod uwagę, że zmiana stanu została już umieszczona w kolejce, jest to tylko kwestia obsługi zdarzeń skupienia w wyzwalaczach dialogu:

To powinno wyglądać znajomo
$('a').on({
  focusout: function () {
    $(this.hash).data('timer', setTimeout(function () {
      $(this.hash).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this.hash).data('timer'));  
  }
});


Esc klucz

Jeśli uważasz, że skończyłeś przez obsługę stanów skupienia, możesz zrobić więcej, aby uprościć obsługę.

Jest to często funkcja „miło mieć”, ale często, gdy masz modalne lub wyskakujące okienko, Escklucz go zamyka.

keydown: function (e) {
  if (e.which === 27) {
    $(this).removeClass('active');
    e.preventDefault();
  }
}


Jeśli wiesz, że w oknie dialogowym znajdują się elementy, które można aktywować, nie trzeba bezpośrednio ustawiać ostrości w oknie dialogowym. Jeśli budujesz menu, możesz zamiast tego skoncentrować pierwszy element menu.

click: function (e) {
  $(this.hash)
    .toggleClass('submenu--active')
    .find('a:first')
    .focus();
  e.preventDefault();
}


WAI-ARIA Obsługa ról i inne ułatwienia dostępu

Mam nadzieję, że ta odpowiedź obejmuje podstawy dostępnej obsługi klawiatury i myszy dla tej funkcji, ale ponieważ jest już dość spora, uniknę dyskusji o rolach i atrybutach WAI-ARIA , jednak gorąco polecam, aby implementatorzy odnieśli się do specyfikacji w celu uzyskania szczegółowych informacji na temat ról, których powinni używać i wszelkich innych odpowiednich atrybutów.

zzzzBov
źródło
29
Jest to najbardziej kompletna odpowiedź, z uwzględnieniem wyjaśnień i dostępności. Myślę, że powinna to być zaakceptowana odpowiedź, ponieważ większość innych odpowiedzi obsługuje tylko kliknięcie i jest po prostu fragmentem kodu usuwanym bez żadnych wyjaśnień.
Cyrille,
4
Niesamowite, dobrze wyjaśnione. Właśnie zastosowałem to podejście w React Component i działałem idealnie dzięki
David Lavieri,
2
@zzzzBov dziękuję za głęboką odpowiedź, staram się to osiągnąć w waniliowym JS i jestem trochę zagubiony przy wszystkich tych jquery. czy jest coś podobnego w waniliowej js?
HendrikEng
2
@zzzzBov nie nie szukam oczywiście napisania wersji wolnej od jQuery, spróbuję to zrobić i chyba najlepiej zadać tutaj nowe pytanie, jeśli naprawdę utknę. Jeszcze raz wielkie dzięki.
HendrikEng
7
Chociaż jest to świetna metoda wykrywania kliknięć niestandardowych menu rozwijanych lub innych danych wejściowych, nigdy nie powinna być preferowaną metodą dla modów lub wyskakujących okienek z dwóch powodów. Modal zostanie zamknięty, gdy użytkownik przełączy się na inną kartę lub okno lub otworzy menu kontekstowe, co jest naprawdę denerwujące. Ponadto zdarzenie „kliknięcie” jest uruchamiane po kliknięciu myszą w górę, a zdarzenie „focusout” uruchamia natychmiastowe naciśnięcie przycisku myszy. Zwykle przycisk wykonuje akcję tylko wtedy, gdy naciśniesz przycisk myszy, a następnie zwolnisz. Właściwym, dostępnym sposobem na dokonanie tego w przypadku modali jest dodanie przycisku zamykania z zakładką.
Kevin
144

Inne rozwiązania tutaj nie działały, więc musiałem użyć:

if(!$(event.target).is('#foo'))
{
    // hide menu
}
Dennis
źródło
Opublikowałem kolejny praktyczny przykład użycia event.target, aby uniknąć wyzwalania innych widgetów interfejsu użytkownika Jquery poza kliknięciami modułów obsługi HTML podczas osadzania ich we własnym wyskakującym oknie: Najlepszy sposób na uzyskanie oryginalnego celu
Joey T.
43
Działa to dla mnie, tyle że dodałem && !$(event.target).parents("#foo").is("#foo")wewnątrz IFinstrukcji, aby żadne elementy potomne nie zamykały menu po kliknięciu.
honyovk
1
Nie można znaleźć lepszego niż: // Jesteśmy poza $ (event.target). Rodzice ('# foo'). Długość == 0
AlexG
3
Jest zwięzłe ulepszenie obsługi głębokiego zagnieżdżania .is('#foo, #foo *'), ale nie polecam wiązania procedur obsługi kliknięć, aby rozwiązać ten problem .
zzzzBov,
1
!$(event.target).closest("#foo").lengthbyłoby lepiej i eliminuje potrzebę dodawania @ honyovk.
tvanc
127

Mam aplikację, która działa podobnie do przykładu Erana, z tym wyjątkiem, że dołączam zdarzenie click do ciała, kiedy otwieram menu ... Trochę tak:

$('#menucontainer').click(function(event) {
  $('html').one('click',function() {
    // Hide the menus
  });

  event.stopPropagation();
});

Więcej informacji o funkcji jQueryone()

Joe Lencioni
źródło
9
ale jeśli klikniesz samo menu, to na zewnątrz nie zadziała :)
vsync
3
Pomaga umieścić event.stopProgagantion () przed powiązaniem detektora kliknięć z ciałem.
Jasper Kennis,
4
Problem polega na tym, że „jeden” dotyczy metody jQuery polegającej na wielokrotnym dodawaniu zdarzeń do tablicy. Więc jeśli klikniesz menu, aby je otworzyć więcej niż raz, zdarzenie jest ponownie powiązane z ciałem i próbuje ukryć menu wiele razy. Aby rozwiązać ten problem, należy zastosować zabezpieczenie przed awarią.
marksyzm
Po powiązaniu za pomocą .one- wewnątrz modułu $('html')obsługi - napisz $('html').off('click').
Cody,
4
@Cody Nie sądzę, że to pomaga. Program oneobsługi wywoła automatycznie off(jak pokazano w dokumentach jQuery).
Mariano Desanze,
44

Po badaniach znalazłem trzy działające rozwiązania (zapomniałem linków do stron w celach informacyjnych)

Pierwsze rozwiązanie

<script>
    //The good thing about this solution is it doesn't stop event propagation.

    var clickFlag = 0;
    $('body').on('click', function () {
        if(clickFlag == 0) {
            console.log('hide element here');
            /* Hide element here */
        }
        else {
            clickFlag=0;
        }
    });
    $('body').on('click','#testDiv', function (event) {
        clickFlag = 1;
        console.log('showed the element');
        /* Show the element */
    });
</script>

Drugie rozwiązanie

<script>
    $('body').on('click', function(e) {
        if($(e.target).closest('#testDiv').length == 0) {
           /* Hide dropdown here */
        }
    });
</script>

Trzecie rozwiązanie

<script>
    var specifiedElement = document.getElementById('testDiv');
    document.addEventListener('click', function(event) {
        var isClickInside = specifiedElement.contains(event.target);
        if (isClickInside) {
          console.log('You clicked inside')
        }
        else {
          console.log('You clicked outside')
        }
    });
</script>
Rameez Rami
źródło
8
Trzecie rozwiązanie jest zdecydowanie najbardziej eleganckim sposobem sprawdzania. Nie wiąże się to również z żadnym obciążeniem jQuery. Bardzo dobrze. To bardzo pomogło. Dzięki.
dbarth
Próbuję uruchomić trzecie rozwiązanie z wieloma elementami document.getElementsByClassName, jeśli ktoś ma jakąś wskazówkę, proszę się nią podzielić.
lowtechsun
@lowtechsun Będziesz musiał przejść przez pętlę, aby sprawdzić każdy.
Donnie D'Amato
Naprawdę podobało mi się trzecie rozwiązanie, jednak kliknięcie uruchamia się, zanim mój div zaczął pokazywać, który to ukrywa ponownie, jakiś pomysł, dlaczego?
indiehjaerta
Trzeci, oczywiście, pozwala na plik console.log, ale nie pozwala go zamknąć, ustawiając opcję display na none - ponieważ zrobi to przed wyświetleniem menu. Czy masz na to rozwiązanie?
RolandiXor,
40
$("#menuscontainer").click(function() {
    $(this).focus();
});
$("#menuscontainer").blur(function(){
    $(this).hide();
});

Działa dla mnie dobrze.

użytkownik212621
źródło
3
To jest to, którego użyłbym. Może nie jest idealny, ale jako programista hobbystyczny jest wystarczająco prosty, aby jasno zrozumieć.
kevtrout
rozmycie jest ruchem poza #menucontainerpytaniem dotyczyło kliknięcia
bor
4
Rozmycie @borrel nie jest ruchem poza kontenerem. Rozmycie jest przeciwieństwem skupienia, myślisz o myszy. To rozwiązanie zadziałało szczególnie dobrze, gdy tworzyłem tekst „kliknij, aby edytować”, w którym zamieniałem się między zwykłym tekstem a polami wejściowymi kliknięć.
parker.sikand
Musiałem dodać tabindex="-1"do #menuscontainer, aby to działało. Wygląda na to, że jeśli umieścisz znacznik wejściowy w pojemniku i klikniesz na niego, pojemnik zostanie ukryty.
tyrion
event mouseleave jest bardziej odpowiedni dla menu i pojemników (patrz: w3schools.com/jquery/… )
Evgenia Manolova
38

Teraz jest do tego wtyczka: zdarzenia zewnętrzne ( post na blogu )

Następujące dzieje się, gdy moduł obsługi Clickoutside (WLOG) jest powiązany z elementem:

  • element jest dodawany do tablicy, która przechowuje wszystkie elementy z modułami obsługi Clickout
  • moduł obsługi kliknięć (z przestrzenią nazw ) jest powiązany z dokumentem (jeśli jeszcze go nie ma)
  • przy każdym kliknięciu w dokumencie uruchamiane jest zdarzenie clickout dla tych elementów w tablicy, które nie są równe lub są elementem nadrzędnym elementu docelowego zdarzenia kliknięcia
  • dodatkowo event.target dla zdarzenia clickoutside jest ustawiony na element, który kliknął użytkownik (abyś wiedział, co kliknął użytkownik, nie tylko to, że kliknął na zewnątrz)

W związku z tym żadne zdarzenia nie są zatrzymywane przed propagacją i dodatkowe funkcje obsługi kliknięć mogą być używane „nad” elementem z zewnętrzną funkcją obsługi.

Wolfram
źródło
Ładna wtyczka. Link do „wydarzeń zewnętrznych” jest martwy, podczas gdy link do wpisu na blogu jest zamiast tego żywy i zapewnia bardzo przydatną wtyczkę do wydarzeń podobnych do „kliknięć”. Jest także licencjonowany przez MIT.
TechNyquist 11.04.17
Świetna wtyczka. Działał idealnie. Sposób użycia jest taki:$( '#element' ).on( 'clickoutside', function( e ) { .. } );
Gavin
33

To działało dla mnie idealnie !!

$('html').click(function (e) {
    if (e.target.id == 'YOUR-DIV-ID') {
        //do something
    } else {
        //do something
    }
});
srinath
źródło
To rozwiązanie działało dobrze w przypadku tego, co robiłem, gdzie nadal potrzebowałem zdarzenia kliknięcia, aby się rozwinąć, dzięki.
Mike
26

Nie sądzę, że tak naprawdę potrzebujesz zamknąć menu, gdy użytkownik kliknie na zewnątrz; potrzebne jest zamknięcie menu, gdy użytkownik kliknie dowolne miejsce na stronie. Po kliknięciu menu lub wyłączeniu menu powinno się zamknąć, prawda?

Nie znalazłem powyżej zadowalających odpowiedzi skłoniło mnie do napisania tego postu na blogu innego dnia. Dla bardziej pedantycznych jest kilka błędów, na które należy zwrócić uwagę:

  1. Jeśli dołączasz moduł obsługi zdarzenia kliknięcia do elementu body w czasie kliknięcia, poczekaj na drugie kliknięcie przed zamknięciem menu i rozpięciem zdarzenia. W przeciwnym razie zdarzenie kliknięcia, które otworzyło menu, spowoduje wyświetlenie bąbelka dla słuchacza, który musi zamknąć menu.
  2. Jeśli użyjesz zdarzenia event.stopPropogation () w przypadku zdarzenia kliknięcia, żadne inne elementy na stronie nie będą miały funkcji „kliknij, aby zamknąć”.
  3. Dołączanie modułu obsługi zdarzeń kliknięcia do elementu body w nieskończoność nie jest wydajnym rozwiązaniem
  4. Porównując cel zdarzenia i jego rodziców z twórcą modułu obsługi, zakłada się, że chcesz zamknąć menu po kliknięciu, a tak naprawdę chcesz to zamknąć po kliknięciu w dowolnym miejscu na stronie.
  5. Odsłuchiwanie zdarzeń w elemencie body sprawi, że Twój kod będzie bardziej kruchy. Stylowanie tak niewinne, jak mogłoby to zepsuć:body { margin-left:auto; margin-right: auto; width:960px;}
34m0
źródło
2
„Po kliknięciu menu lub wyłączeniu menu powinno ono zamknąć się, prawda?” nie zawsze. Anulowanie kliknięcia poprzez przeciągnięcie elementu spowoduje nadal kliknięcie na poziomie dokumentu, ale intencją nie byłoby dalsze zamykanie menu. Istnieje również wiele innych rodzajów okien dialogowych, w których można użyć zachowania „kliknięcia”, które pozwoliłoby na kliknięcie wewnętrzne.
zzzzBov,
25

Jak napisano w innym plakacie, jest wiele gotchas, zwłaszcza jeśli wyświetlany element (w tym przypadku menu) zawiera elementy interaktywne. Znalazłem następującą metodę, która jest dość solidna:

$('#menuscontainer').click(function(event) {
    //your code that shows the menus fully

    //now set up an event listener so that clicking anywhere outside will close the menu
    $('html').click(function(event) {
        //check up the tree of the click target to check whether user has clicked outside of menu
        if ($(event.target).parents('#menuscontainer').length==0) {
            // your code to hide menu

            //this event listener has done its job so we can unbind it.
            $(this).unbind(event);
        }

    })
});
Ben B
źródło
25

Prostym rozwiązaniem tej sytuacji jest:

$(document).mouseup(function (e)
{
    var container = $("YOUR SELECTOR"); // Give you class or ID

    if (!container.is(e.target) &&            // If the target of the click is not the desired div or section
        container.has(e.target).length === 0) // ... nor a descendant-child of the container
    {
        container.hide();
    }
});

Powyższy skrypt ukryje, divjeśli divzdarzenie poza kliknięciem zostanie wyzwolone.

Aby uzyskać więcej informacji, zobacz następujący blog: http://www.codecanal.com/detect-click-outside-div-using-javascript/

Damit Jitendra
źródło
22

Rozwiązanie 1

Zamiast używać event.stopPropagation (), który może mieć pewne skutki uboczne, wystarczy zdefiniować prostą zmienną flagi i dodać jeden ifwarunek. Przetestowałem to i działałem poprawnie bez żadnych skutków ubocznych zatrzymania Rozmnażanie:

var flag = "1";
$('#menucontainer').click(function(event){
    flag = "0"; // flag 0 means click happened in the area where we should not do any action
});

$('html').click(function() {
    if(flag != "0"){
        // Hide the menus if visible
    }
    else {
        flag = "1";
    }
});

Rozwiązanie 2

Z prostym ifwarunkiem:

$(document).on('click', function(event){
    var container = $("#menucontainer");
    if (!container.is(event.target) &&            // If the target of the click isn't the container...
        container.has(event.target).length === 0) // ... nor a descendant of the container
    {
        // Do whatever you want to do when click is outside the element
    }
});
Iman Sedighi
źródło
Użyłem tego rozwiązania z flagą boolowską i jest to również dobre z przegubowym DOm, a także jeśli w #menucontainer jest wiele innych elementów
Migio B
Rozwiązanie 1 działa lepiej, ponieważ obsługuje przypadki, gdy cel kliknięcia jest usuwany z DOM przed zdarzeniem propagowanym do dokumentu.
Alice
21

Sprawdź cel zdarzenia kliknięcia w oknie (powinien się propagować do okna, o ile nie zostanie przechwycony nigdzie indziej) i upewnij się, że nie jest to żaden z elementów menu. Jeśli nie, oznacza to, że jesteś poza swoim menu.

Lub sprawdź pozycję kliknięcia i sprawdź, czy jest ono zawarte w obszarze menu.

Chris MacDonald
źródło
18

Miałem sukces z czymś takim:

var $menuscontainer = ...;

$('#trigger').click(function() {
  $menuscontainer.show();

  $('body').click(function(event) {
    var $target = $(event.target);

    if ($target.parents('#menuscontainer').length == 0) {
      $menuscontainer.hide();
    }
  });
});

Logika jest następująca: gdy #menuscontainerjest pokazany, powiąż moduł obsługi kliknięć z ciałem, które ukrywa się #menuscontainertylko wtedy, gdy cel (kliknięcia) nie jest jego dzieckiem.

Chu Yeow
źródło
17

Jako wariant:

var $menu = $('#menucontainer');
$(document).on('click', function (e) {

    // If element is opened and click target is outside it, hide it
    if ($menu.is(':visible') && !$menu.is(e.target) && !$menu.has(e.target).length) {
        $menu.hide();
    }
});

Nie ma problemu z zatrzymaniem propagacji zdarzeń i lepiej obsługuje wiele menu na tej samej stronie, gdzie kliknięcie drugiego menu, gdy pierwsze jest otwarte, pozostawi pierwsze otwarte w rozwiązaniu stopPropagation.

Bohdan Lyzanets
źródło
16

Zdarzenie ma właściwość o nazwie event.path elementu, który jest „statyczną uporządkowaną listą wszystkich jego przodków w kolejności drzew” . Aby sprawdzić, czy zdarzenie pochodzi z określonego elementu DOM lub jednego z jego elementów potomnych, po prostu sprawdź ścieżkę do tego konkretnego elementu DOM. Można go również użyć do sprawdzenia wielu elementów poprzez logiczne ORsprawdzenie kontroli elementu w somefunkcji.

$("body").click(function() {
  target = document.getElementById("main");
  flag = event.path.some(function(el, i, arr) {
    return (el == target)
  })
  if (flag) {
    console.log("Inside")
  } else {
    console.log("Outside")
  }
});
#main {
  display: inline-block;
  background:yellow;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="main">
  <ul>
    <li>Test-Main</li>
    <li>Test-Main</li>
    <li>Test-Main</li>
    <li>Test-Main</li>
    <li>Test-Main</li>
  </ul>
</div>
<div id="main2">
  Outside Main
</div>

Tak powinno być w twoim przypadku

$("body").click(function() {
  target = $("#menuscontainer")[0];
  flag = event.path.some(function(el, i, arr) {
    return (el == target)
  });
  if (!flag) {
    // Hide the menus
  }
});
Dan Philip
źródło
1
Nie działa w Edge.
Legendy,
event.pathto nie jest rzecz.
Andrew
14

Znalazłem tę metodę w jakiejś wtyczce kalendarza jQuery.

function ClickOutsideCheck(e)
{
  var el = e.target;
  var popup = $('.popup:visible')[0];
  if (popup==undefined)
    return true;

  while (true){
    if (el == popup ) {
      return true;
    } else if (el == document) {
      $(".popup").hide();
      return false;
    } else {
      el = $(el).parent()[0];
    }
  }
};

$(document).bind('mousedown.popup', ClickOutsideCheck);
nazar kuliyev
źródło
13

Oto waniliowe rozwiązanie JavaScript dla przyszłych przeglądających.

Po kliknięciu dowolnego elementu w dokumencie, jeśli identyfikator klikniętego elementu jest przełączany lub ukryty element nie jest ukryty, a ukryty element nie zawiera klikniętego elementu, przełącz element.

(function () {
    "use strict";
    var hidden = document.getElementById('hidden');
    document.addEventListener('click', function (e) {
        if (e.target.id == 'toggle' || (hidden.style.display != 'none' && !hidden.contains(e.target))) hidden.style.display = hidden.style.display == 'none' ? 'block' : 'none';
    }, false);
})();

Jeśli zamierzasz mieć wiele przełączników na tej samej stronie, możesz użyć czegoś takiego:

  1. Dodaj nazwę klasy hiddendo elementu składanego.
  2. Po kliknięciu dokumentu zamknij wszystkie ukryte elementy, które nie zawierają klikniętego elementu i nie są ukryte
  3. Jeśli kliknięty element jest przełącznikiem, przełącz określony element.

(function () {
    "use strict";
    var hiddenItems = document.getElementsByClassName('hidden'), hidden;
    document.addEventListener('click', function (e) {
        for (var i = 0; hidden = hiddenItems[i]; i++) {
            if (!hidden.contains(e.target) && hidden.style.display != 'none')
                hidden.style.display = 'none';
        }
        if (e.target.getAttribute('data-toggle')) {
            var toggle = document.querySelector(e.target.getAttribute('data-toggle'));
            toggle.style.display = toggle.style.display == 'none' ? 'block' : 'none';
        }
    }, false);
})();
<a href="javascript:void(0)" data-toggle="#hidden1">Toggle Hidden Div</a>
<div class="hidden" id="hidden1" style="display: none;" data-hidden="true">This content is normally hidden</div>
<a href="javascript:void(0)" data-toggle="#hidden2">Toggle Hidden Div</a>
<div class="hidden" id="hidden2" style="display: none;" data-hidden="true">This content is normally hidden</div>
<a href="javascript:void(0)" data-toggle="#hidden3">Toggle Hidden Div</a>
<div class="hidden" id="hidden3" style="display: none;" data-hidden="true">This content is normally hidden</div>

użytkowników4639281
źródło
13

Dziwi mnie, że nikt tak naprawdę nie potwierdził focusoutwydarzenia:

var button = document.getElementById('button');
button.addEventListener('click', function(e){
  e.target.style.backgroundColor = 'green';
});
button.addEventListener('focusout', function(e){
  e.target.style.backgroundColor = '';
});
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
</head>
<body>
  <button id="button">Click</button>
</body>
</html>

Jovanni G.
źródło
Chyba brakuje ci mojej odpowiedzi. stackoverflow.com/a/47755925/6478359
Muhammet Can TONBUL
To powinna być zaakceptowana odpowiedź, od razu do rzeczy. Dzięki!!!
Lucky Lam
8

Jeśli piszesz skrypty dla IE i FF 3. * i chcesz po prostu wiedzieć, czy kliknięcie nastąpiło w określonym obszarze pola, możesz również użyć czegoś takiego:

this.outsideElementClick = function(objEvent, objElement){   
var objCurrentElement = objEvent.target || objEvent.srcElement;
var blnInsideX = false;
var blnInsideY = false;

if (objCurrentElement.getBoundingClientRect().left >= objElement.getBoundingClientRect().left && objCurrentElement.getBoundingClientRect().right <= objElement.getBoundingClientRect().right)
    blnInsideX = true;

if (objCurrentElement.getBoundingClientRect().top >= objElement.getBoundingClientRect().top && objCurrentElement.getBoundingClientRect().bottom <= objElement.getBoundingClientRect().bottom)
    blnInsideY = true;

if (blnInsideX && blnInsideY)
    return false;
else
    return true;}
Erik
źródło
8

Zamiast używać przerwania przepływu, zdarzenia rozmycia / skupienia lub innych trudnych technik, po prostu dopasuj przepływ zdarzeń do pokrewieństwa elementu:

$(document).on("click.menu-outside", function(event){
    // Test if target and it's parent aren't #menuscontainer
    // That means the click event occur on other branch of document tree
    if(!$(event.target).parents().andSelf().is("#menuscontainer")){
        // Click outisde #menuscontainer
        // Hide the menus (but test if menus aren't already hidden)
    }
});

Aby usunąć kliknięcie zewnętrznego detektora zdarzeń, po prostu:

$(document).off("click.menu-outside");
memy
źródło
Dla mnie było to najlepsze rozwiązanie. Użyłem go w wywołaniu zwrotnym po animacji, więc musiałem jakoś odłączyć zdarzenie. +1
qwertzman
Tutaj jest nadmiarowy czek. jeśli element jest rzeczywiście #menuscontainer, wciąż przechodzicie przez jego rodziców. powinieneś to najpierw sprawdzić, a jeśli to nie jest ten element, to przejdź do drzewa DOM.
vsync
Dobrze ! Możesz zmienić warunek na if(!($(event.target).is("#menuscontainer") || $(event.target).parents().is("#menuscontainer"))){. Jest to drobna optymalizacja, ale występuje tylko kilka razy w trakcie trwania programu: dla każdego kliknięcia, jeśli click.menu-outsidezdarzenie jest zarejestrowane. Jest dłuższy (+32 znaków) i nie używaj łączenia metod
mem
8

Posługiwać się:

var go = false;
$(document).click(function(){
    if(go){
        $('#divID').hide();
        go = false;
    }
})

$("#divID").mouseover(function(){
    go = false;
});

$("#divID").mouseout(function (){
    go = true;
});

$("btnID").click( function(){
    if($("#divID:visible").length==1)
        $("#divID").hide(); // Toggle
    $("#divID").show();
});
webenformasyon
źródło
7

Jeśli ktoś tutaj jest ciekawy to rozwiązanie javascript (es6):

window.addEventListener('mouseup', e => {
        if (e.target != yourDiv && e.target.parentNode != yourDiv) {
            yourDiv.classList.remove('show-menu');
            //or yourDiv.style.display = 'none';
        }
    })

i es5, na wszelki wypadek:

window.addEventListener('mouseup', function (e) {
if (e.target != yourDiv && e.target.parentNode != yourDiv) {
    yourDiv.classList.remove('show-menu'); 
    //or yourDiv.style.display = 'none';
}

});

Walt
źródło
7

Oto proste rozwiązanie według czystego javascript. Jest na bieżąco z ES6 :

var isMenuClick = false;
var menu = document.getElementById('menuscontainer');
document.addEventListener('click',()=>{
    if(!isMenuClick){
       //Hide the menu here
    }
    //Reset isMenuClick 
    isMenuClick = false;
})
menu.addEventListener('click',()=>{
    isMenuClick = true;
})
Duannx
źródło
„Na bieżąco z ES6” to dość śmiałe stwierdzenie, gdy () => {}zamiast tego robi się tylko ES6 function() {}. To, co tam masz, jest klasyfikowane jako zwykły JavaScript z dodatkiem ES6.
MortenMoulder
@MortenMoulder: Ya. To tylko dla uwagi, mimo że w rzeczywistości jest to ES6. Ale spójrz tylko na rozwiązanie. Myślę, że jest dobrze.
Duannx,
To waniliowy JS i działa na cel zdarzenia usunięty z DOM (np. Po wybraniu wartości z wewnętrznego wyskakującego okienka, natychmiastowe zamknięcie wyskakującego okienka). +1 ode mnie!
Alice
6

Zaczep detektor zdarzeń kliknięcia w dokumencie. Wewnątrz detektora zdarzeń możesz spojrzeć na obiekt zdarzenia , w szczególności event.target, aby zobaczyć, który element został kliknięty:

$(document).click(function(e){
    if ($(e.target).closest("#menuscontainer").length == 0) {
        // .closest can help you determine if the element 
        // or one of its ancestors is #menuscontainer
        console.log("hide");
    }
});
Salman A.
źródło
6

Użyłem poniżej skryptu i skończyłem z jQuery.

jQuery(document).click(function(e) {
    var target = e.target; //target div recorded
    if (!jQuery(target).is('#tobehide') ) {
        jQuery(this).fadeOut(); //if the click element is not the above id will hide
    }
})

Poniżej znajdź kod HTML

<div class="main-container">
<div> Hello I am the title</div>
<div class="tobehide">I will hide when you click outside of me</div>
</div>

Samouczek możesz przeczytać tutaj

Rinto George
źródło
5
$(document).click(function() {
    $(".overlay-window").hide();
});
$(".overlay-window").click(function() {
    return false;
});

Jeśli klikniesz dokument, ukryj dany element, chyba że klikniesz ten sam element.

Jarzębina
źródło
5

Głosuj za najpopularniejszą odpowiedzią, ale dodaj

&& (e.target != $('html').get(0)) // ignore the scrollbar

więc kliknięcie paska przewijania nie powoduje [ukrycia ani żadnego innego] elementu docelowego.

bbe
źródło
5

Dla łatwiejszego użycia i bardziej ekspresyjnego kodu stworzyłem wtyczkę jQuery w tym celu:

$('div.my-element').clickOut(function(target) { 
    //do something here... 
});

Uwaga: cel to element, który użytkownik kliknął. Ale wywołanie zwrotne jest nadal wykonywane w kontekście oryginalnego elementu, więc możesz użyć tego, jak można oczekiwać w wywołaniu zwrotnym jQuery.

Podłącz:

$.fn.clickOut = function (parent, fn) {
    var context = this;
    fn = (typeof parent === 'function') ? parent : fn;
    parent = (parent instanceof jQuery) ? parent : $(document);

    context.each(function () {
        var that = this;
        parent.on('click', function (e) {
            var clicked = $(e.target);
            if (!clicked.is(that) && !clicked.parents().is(that)) {
                if (typeof fn === 'function') {
                    fn.call(that, clicked);
                }
            }
        });

    });
    return context;
};

Domyślnie detektor zdarzenia kliknięcia jest umieszczany w dokumencie. Jeśli jednak chcesz ograniczyć zasięg detektora zdarzeń, możesz przekazać obiekt jQuery reprezentujący element poziomu nadrzędnego, który będzie najwyższym elementem nadrzędnym, w którym będą nasłuchiwane kliknięcia. Zapobiega to niepotrzebnemu nasłuchiwaniu zdarzeń na poziomie dokumentu. Oczywiście nie będzie działać, chyba że podany element nadrzędny jest rodzicem elementu początkowego.

Użyj tak:

$('div.my-element').clickOut($('div.my-parent'), function(target) { 
    //do something here...
});
Matt Matt Goodwin
źródło