click-within javascript test
beatovich
- javascript
hallo
Ist der Click in oder ausserhalb <nav> erfolgt?
Um das zu testen, muss ich notwendigerweise einen Handler auf body registrieren.
document.body.addEventListener("click", function(ev){
// ich habe ev.target
// was ist die schnellste Methode um zu testen,
// ob zwischen ev.target und body ein <nav> liegt?
});
Könnt ihr mir einen Tipp geben?
Morgen,
Ist der Click in oder ausserhalb <nav> erfolgt?
Um das zu testen, muss ich notwendigerweise einen Handler auf body registrieren.
document.body.addEventListener("click", function(ev){ // ich habe ev.target // was ist die schnellste Methode um zu testen, // ob zwischen ev.target und body ein <nav> liegt? });
Könnt ihr mir einen Tipp geben?
Du brauchst nicht im Eventlistener den gesamten <body>
durchsuchen, da dieser immer ausgeführt wird, ist das nicht performant. Du solltest den Eventhandler direkt auf ein bestimmtes Element oder eine Elementgruppe registrieren.
var element = document.getElementById('navID')
// oder
var element = document.getElementsByClassName('class')
//oder
var element = documentgetElementByTagName('nav')
if (element) {
element.addEventListener('click', function(event){
// do something
})
}
Gruß
Jo
hallo
Ist der Click in oder ausserhalb <nav> erfolgt?
Um das zu testen, muss ich notwendigerweise einen Handler auf body registrieren.
document.body.addEventListener("click", function(ev){ // ich habe ev.target // was ist die schnellste Methode um zu testen, // ob zwischen ev.target und body ein <nav> liegt? });
Könnt ihr mir einen Tipp geben?
Du brauchst nicht im Eventlistener den gesamten
<body>
durchsuchen, da dieser immer ausgeführt wird, ist das nicht performant. Du solltest den Eventhandler direkt auf ein bestimmtes Element oder eine Elementgruppe registrieren.
Typische Anwendung:
Du klickst ausserhalb einer nav, und die soll sich dann schliessen.
Es bringt hier nichts, auf der Navigation einen Eventhandler zu registrieren, da er gar nicht feuern wird, wenn ausserhalb der nav geklickt wird.
Hey,
Typische Anwendung:
Du klickst ausserhalb einer nav, und die soll sich dann schliessen.
Es bringt hier nichts, auf der Navigation einen Eventhandler zu registrieren, da er gar nicht feuern wird, wenn ausserhalb der nav geklickt wird.
Ganz schliessen? Und beim nächsten Klick soll sie wieder auftauchen? Also bei jedem Klick öffenen bzw. schliessen?
Das wär ja richtig nervig. Dafür sollte es einen Button geben.
Gruß
Jo
hallo
Ganz schliessen? Und beim nächsten Klick soll sie wieder auftauchen? Also bei jedem Klick öffenen bzw. schliessen?
Was soll nervig sein daran, wenn sich die Untermenus einer Navigation schliessen, wenn du ausserhalb der Navigation klickst? 99.9% aller Navigationen verhalten sich so.
Das wär ja richtig nervig. Dafür sollte es einen Button geben.
Nö. für das automatische Schliessen soll es keinen Button geben. Das wäre nämlich nervig.
Also es wäre nett, zu meiner Frage zurückzukehren, und nicht nicht existente Probleme zu produzieren.
hallo
Ganz schliessen? Und beim nächsten Klick soll sie wieder auftauchen? Also bei jedem Klick öffenen bzw. schliessen?
Was soll nervig sein daran, wenn sich die Untermenus einer Navigation schliessen, wenn du ausserhalb der Navigation klickst? 99.9% aller Navigationen verhalten sich so.
Es geht ja auch nicht um die Untermenüs sondern um die gesamte Navigation. Für die Untermenüs braucht man kein Javascript, sondern kann einzig mit CSS diese schliessen und öffnen.
Das wär ja richtig nervig. Dafür sollte es einen Button geben.
Nö. für das automatische Schliessen soll es keinen Button geben. Das wäre nämlich nervig.
Also es wäre nett, zu meiner Frage zurückzukehren, und nicht nicht existente Probleme zu produzieren.
Gut, dann such in deinem Eventlistener nach dem Element und frage ob es da ist. Bleibt bei einer Kombination aus deinem und meinem Teil, wenn du es etwas umstellst.
Gruß
Jo
@@J o
Für die Untermenüs braucht man kein Javascript, sondern kann einzig mit CSS diese schliessen und öffnen.
Daran sind schon etliche gescheitert. Das hatten wir doch letztens erst.
Eine Nur-mit-CSS-„Lösung“, die nur mit Maus, aber nicht mit Touch und Tastatur bedienenbar ist, ist keine Lösung.
LLAP 🖖
hallo
Es geht ja auch nicht um die Untermenüs sondern um die gesamte Navigation. Für die Untermenüs braucht man kein Javascript, sondern kann einzig mit CSS diese schliessen und öffnen.
Hast du ein Beispiel für eine in allen Belangen funktionierende und zugängliche CSS-only Navigation mit Untermenus?
Meines Wissens brauchst du (heute) Javascript
Ich lass mich aber gerne eines Besseren belehren.
Hallo beatovich,
ich würde das arbeitsteilig (a.k.a. objektorientiert) lösen, indem ich einen click-Handler auf nav und body registriere. Der im nav macht das Menü bei Bedarf auf und stoppt die Propagierung. Der im Body kann dann davon ausgehen, dass das click nicht aus dem nav-Bereich kommt, und macht einfach zu.
Rolf
hallo
Hallo beatovich,
ich würde das arbeitsteilig (a.k.a. objektorientiert) lösen, indem ich einen click-Handler auf nav und body registriere. Der im nav macht das Menü bei Bedarf auf und stoppt die Propagierung. Der im Body kann dann davon ausgehen, dass das click nicht aus dem nav-Bereich kommt, und macht einfach zu.
Wenn ich nichts gescheiteres finde (ausser iterativ ev.target.parent auf nodeName zu überprüfen) dann werde ich wohl https://stackoverflow.com/questions/152975/how-do-i-detect-a-click-outside-an-element# anwenden auch wenn https://css-tricks.com/dangers-stopping-event-propagation/ Philip Walton gerade davon abrät.
Für mich ist relevant, dass mein JS als Framework zu betrachten ist. Ich würde also lieber was anderes nehmen.
Hey,
Wenn ich nichts gescheiteres finde (ausser iterativ ev.target.parent auf nodeName zu überprüfen) dann werde ich wohl https://stackoverflow.com/questions/152975/how-do-i-detect-a-click-outside-an-element# anwenden auch wenn https://css-tricks.com/dangers-stopping-event-propagation/ Philip Walton gerade davon abrät.
Wenn, dann würde ich die Navigation explizit im Eventlistener suchen.
document.body.addEventListener('click', function(event){
var element = document.getElementsByTagName('nav')
if (element) {
// do something
})
}
Gruß
Jo
hallo
Wenn, dann würde ich die Navigation explizit im Eventlistener suchen.
document.body.addEventListener('click', function(ev){
var el = ev.target, c = true;
while (c==true) {
if(el.nodeName == "NAV"){c=false; return }
if(el.nodeName == "BODY"){c=false; closeNav(); return }
el=el.parentElement;
}
});
sollte den Trick machen. Nur ist das unschön...
Hallo beatovich,
so ist's vielleicht schöner (hoffentlich auch korrekt... 😉 ).
document.body.addEventListener('click', function(ev){
for (var el=ev.target; el && el.NodeName != "BODY"; el=el.parentElement);
if(el.nodeName == "NAV") return;
}
closeNav();
});
Du hast damit bei jedem Klick eine Bottom-Up Suche durch das DOM. Da deine Seite keinen Weltrekord im Schnellklicken anstrebt, sollte das egal sein.
Man könnte auch einen Top-Down Ansatz wählen:
document.body.addEventListener('click', function(ev){
var navList = document.getElementsByTagName("NAV");
for (var i=0; i<navList.length; i++)
if (navList[i].contains(ev.target) return;
closeNav();
}
damit verwendest Du eine Menge nativer Browserfunktionen, aber ob das schneller ist als deine Parent-Suche? Keine Ahnung. Wenn contains
schlau ist, sucht es vom übergebenen Node aufwärts bis es this oder null findet, und damit tut es das gleiche wie deine eigene Suche. Aber die Suche nach NAV Elementen dürfte eine DOM Traversierung bedeuten. D.h. die contains Idee lohnt nur wenn man genau ein nav hat, auf das reagiert werden soll.
Wenn Du keine Event-Propagierung stoppen willst, könntest Du vielleicht dennoch auf die Doppelregistrierung zurückgreifen. Der click-Handler auf <nav>
könnte ein Extraproperty ans Event-Objekt kleben und der click-Handler am <body>
darauf reagieren. Setzt natürlich voraus, dass nicht nachträglich nav-Elemente eingescriptet werden. Und wenn's mehr als ein nav geben können soll, muss die Registrierung für die nav-Listener in eine Schleife.
var hitNav = new Symbol();
var navList = document.getElementsByTagName("NAV");
for (var i=0; i<navList.length; i++) {
navList[i].addEventListener('click', function(ev) {
ev[hitNav] = true;
});
}
document.body.addEventListener('click', function(ev) {
if (ev[hitNav])
return;
closeNav();
});
Also Fazit: Wenn Du nur ein einziges NAV als relevant ansiehst, könnte die contains-Idee lohnen. Wenn's mehrere sind, bleib bei deiner Bottom-Up suche, die dürfte am performantesten sein. Kannst sie ja in einem Helper kapseln und den als ExtraFill ans HTMLElement kleben:
HTMLElement.prototype.closestParentWithTagName = function(name) {
var el = this.parentElement;
while (el && el.tagName !== name);
el = el.parentElement);
return el;
}
Das geht auch rekursiv - und wenn JavaScript mal echte Tail-Rekursion kriegt, dürfte es sogar effizient werden:
HTMLElement.prototype.closestParentWithTagName = function(name) {
var el = this.parentElement;
return (!el || el.tagName === name) ? el : el.closestParentWithTagName(name));
}
Rolf
hallo
Das sind jetzt eine ganze Menge Vorschläge.
Ich habe mal .contains angewendet und das tut den Job für eine Nav. Ich muss aber im Grunde gar nicht dauernd einen Listener im body registrieren, sondern ein solcher muss nur (wenn nicht bereits vorhanden) bei Click in nav > details registriert werden. Wenn querySelector([open]) undefined ist, kann ich den Listener wieder entfernen.
Hey
HTMLElement.prototype.closestParentWithTagName = function(name) { var el = this.parentElement; while (el && el.tagName !== name); el = el.parentElement); return el; }
Ich finde es irgendwie nicht besonders sinnvoll etwas nachzubauen, was in ähnlicher Form bereits standardisiert und in modernen Browsern implementiert ist.
Es wird ein Zeitpunkt kommen, an dem ein Polyfill für Element.prototype.closest
wieder entfernt werden kann, und wenn es soweit ist, kann man das machen ohne den restlichen Code anfassen zu müssen. Bei einer selbstgebastelten Lösung müssten hingegen auch alle Aufrufe verändert werden, wenn man sich irgendwann entscheidet die native Methode zu nutzen.
Ich sehe auch nicht, dass die Methode gegenüber closest
einen so gewichtigen Vorteil hätte, dass es gerechtfertigt wäre sie auch dann noch einzubinden, wenn die Unterstützung von closest
durch die Browser längst kein Thema mehr ist. Irgendwann wird das dann nur noch mitgeschleppt, weil keiner Lust hat alle Stellen zu suchen an denen es verwendet wird.
while (el && el.tagName !== name); el = el.parentElement);
Allerdings finde ich es gut, dass du hier nach el.parentElement
noch eine schließende runde Klammer eingefügt hast. Das hätte sonst ins Auge gehen können. ;-)
Abgesehen von den bereits genannten, gibt es noch eine weitere Möglichkeit festzustellen, ob eine Aktivierung innerhalb oder außerhalb eines nav
-Elementes erfolgt ist. Nämlich die Methode Event.composedPath
, mittels derer man den jeweiligen Event Path eines Ereignisses lesen kann, also alle Objekte von window
bis zum Zielknoten, die von dem Eventobjekt auf seinem Weg durch’s DOM passiert werden.
const nav = document.querySelector('nav')
document.body.addEventListener('click', event => event.composedPath().includes(nav) && console.info('click in nav'))
Das ist auch insofern interessant, als dass hier nicht – wie bei älteren DOM-Methoden üblich – eine HTMLCollection oder NodeList zurückgegeben wird, die kaum über nützliche Methoden verfügt, sondern stattdessen ein richtiges Array. Das heißt, man kann hier auch ohne vorheriges Umkopieren der Referenzen mit Methoden wie filter
, find
, oder auch includes
auf der Liste operieren.
Und wenn wir schon dabei sind, noch eine besonders unhandliche Variante:
Die Methode Node.compareDocumentPosition(otherNode)
gibt eine Bitmaske zurück, mit der sich unter anderem prüfen lässt, ob das Kontextobjekt und das als Argument übergebene Objekt Vor- oder Nachfahren sind. Damit könnte die Prüfung so aussehen…
const nav = document.querySelector('nav');
document.body.addEventListener('click', function({ target }) {
if (nav.isSameNode(target) || nav.compareDocumentPosition(target) &
Node.DOCUMENT_POSITION_CONTAINED_BY) {
console.info('click in nav');
}
});
…oder anders herum, mit Node.DOCUMENT_POSITION_CONTAINS
. Statt die unnötig sperrigen Eigenschaften auf dem Objekt Node
zu referenzieren, könnte man natürlich auch direkt mit 16 oder 8 verunden. So sieht dann gut lesbarer Code aus. :-D
Das geht auch rekursiv - und wenn JavaScript mal echte Tail-Rekursion kriegt, dürfte es sogar effizient werden
Zumindest sind Tail Position Calls seit ECMAScript 2015 im Standard. Es wird zunächst definiert, wann syntaktisch ein Tail Call vorliegt, und dann unter Runtime Semantics der konkreten Implementierung aufgetragen, alle mit dem aktuellen Funktionsausführungskontext verknüpften internen Ressourcen entweder freizugeben, oder sie für den neuen Aufruf wiederzuverwenden.
Die Browserunterstützung für Tail Call Optimization ist aber in der Tat noch nicht überwältigend. Allerdings ist die maximale Größe des Call Stacks aktueller Engines meines Wissens vergleichsweise komfortabel. Anders als zum Beispiel in Python, wo die Ausführung standardmäßig bei 1000 Aufrufen abgewürgt wird, hat man hier in der Regel größeren Spielraum. Also solange man nicht mit der Ackermannfunktion rechnet…
Viele Grüße,
Orlok
Hallo Orlok,
Element.closest kannte ich nicht und es ist mir beim Umschauen gestern auch nicht aufgefallen. Vielleicht sollte ich mir angewöhnen, mir die Spec unter's Kopfkissen zu legen.
Danke für den Hinweis. Meine Funktion ist damit komplett überflüssig.
Die Klammer fand ich hilfreich, um die Funktion abzurunden....
Rolf
@@Rolf B
so ist's vielleicht schöner (hoffentlich auch korrekt... 😉 ).
document.body.addEventListener('click', function(ev){ for (var el=ev.target; el && el.NodeName != "BODY"; el=el.parentElement); if(el.nodeName == "NAV") return; } closeNav(); });
Ich weiß nicht, was an den Variablenbezeichnern ev
und el
schön sein soll.
Don’t make me think! event
und element
machen lesbaren Code.
LLAP 🖖
@@beatovich
Dafür sollte es einen Button geben. Nö. für das automatische Schliessen soll es keinen Button geben.
Doch. Es ist nicht ersichtlich, dass sich das Menü auf Click außerhalb schließt.
Es sollte beides gehen: das Menü schließt sich auf Click auf den Schließen-Button innerhalb des Menüs sowie auf Click außerhalb des Menüs.
LLAP 🖖
hallo
@@beatovich
Dafür sollte es einen Button geben. Nö. für das automatische Schliessen soll es keinen Button geben.
Doch. Es ist nicht ersichtlich, dass sich das Menü auf Click außerhalb schließt.
Der Button zum schliessen existiert. Aber das ist dann nicht "automatisch", sondern "explizit".
Können wir bitte diese Diskussion abschliessen?
Hey
Könnt ihr mir einen Tipp geben?
Du kannst mit der Methode Element.closest(selectors)
prüfen, ob eines der Vorfahrenelemente (inklusive dem Kontextelement selbst) ein nav
ist.
document.body.addEventListener('click', event => event.target.closest('nav') && console.info('click in nav'))
Wenn closest
kein Element findet, dann wird null
zurückgegeben. Die Methode wird allerdings von einigen (vermutlich) relevanten Browsern noch nicht unterstützt, da müsste also gegebenenfalls noch für einige Zeit ein Polyfill eingebunden werden.
Alternativ kannst du natürlich eine oder mehrere Referenzen auf die nav
-Elemente der Seite speichern und dann mittels Node.contains(otherNode)
vergleichen.
const nav = document.querySelector('nav');
document.body.addEventListener('click', event => nav.contains(event.target) && console.info('click in nav'))
Viele Grüße,
Orlok
hallo
Könnt ihr mir einen Tipp geben?
Du kannst mit der Methode
Element.closest(selectors)
prüfen, ob eines der Vorfahrenelemente (inklusive dem Kontextelement selbst) einnav
ist.document.body.addEventListener('click', event => event.target.closest('nav') && console.info('click in nav'))
Wenn
closest
kein Element findet, dann wirdnull
zurückgegeben. Die Methode wird allerdings von einigen (vermutlich) relevanten Browsern noch nicht unterstützt, da müsste also gegebenenfalls noch für einige Zeit ein Polyfill eingebunden werden.
Das werde ich mir auf jeden Fall mal anschauen. Bis jetzt habe ich das nur als jQuery-Methode gesehen.
Alternativ kannst du natürlich eine oder mehrere Referenzen auf die
nav
-Elemente der Seite speichern und dann mittelsNode.contains(otherNode)
vergleichen.
Auch das ist Neuland.
const nav = document.querySelector('nav'); document.body.addEventListener('click', event => nav.contains(event.target) && console.info('click in nav'))
Ja, das entspricht absolut meiner Vorstellung.
Vielen Dank, das gibt mir neue Forschungsziele. Eventuell brauche ich ja nicht mal ein polyfill, sondern kann für seltene Browser eine alte Methode bereitstellen.
Hey
const nav = document.querySelector('nav'); document.body.addEventListener('click', event => nav.contains(event.target) && console.info('click in nav'))
Ja, das entspricht absolut meiner Vorstellung.
Nachdem ich in den letzten Änderungen gesehen habe, dass du im Wiki bereits einen Link auf die bis dato nicht existierende Seite für die Methode Node.prototype.contains
gesetzt hast, habe ich mir mal erlaubt den dazu passenden Artikel zu schreiben. :-)
Viele Grüße,
Orlok
hallo
Nachdem ich in den letzten Änderungen gesehen habe, dass du im Wiki bereits einen Link auf die bis dato nicht existierende Seite für die Methode
Node.prototype.contains
gesetzt hast, habe ich mir mal erlaubt den dazu passenden Artikel zu schreiben. :-)
Sehr schön.
Ich konnte es mir aber nicht verkneifen, beide Beispiele etwas zu verändern.
Aber die Verwendung des '<output>' zur Kontrollausgabe fand ich didaktisch beispeilhaft. (immer wieder mal verschiedene Möglichkeiten der Verarbeitung demonstrieren).
Hey
Ich konnte es mir aber nicht verkneifen, beide Beispiele etwas zu verändern.
Die Änderung am ersten Beispiel fand ich gut und habe sie weitestgehend so gelassen. Ist echt anschaulicher mit main
statt body
. Nur der Begleittext war etwas verunglückt und hat nicht mehr ganz gepasst, weshalb ich da nochmal eingegriffen habe.
+1
Hier wäre nur allgemein anzumerken, dass ich beim Schreiben von Artikeln versuche nicht zu viele Codeauszeichnungen und Links in die Absätze einzubauen, da sonst der Lesefluss nicht unerheblich beeinträchtigt wird.
Die Änderung am zweiten Beispiel musste ich allerdings leider rückgängig machen, denn es kann nicht unser Ziel sein Codebeispiele zu geben, die nicht mehr den aktuellen best practices entsprechen. Die Sprache entwickelt sich weiter.
document.body.addEventListener('click', function({ target }) {
output.value = `click ${
main.contains(target) ? 'inside' : 'outside'
} main element`;
});
Templateliterale sind ein riesiger Fortschritt gegenüber der früher üblichen Verkettung mit dem Plusoperator und die Destrukturierung von Parametern hilft dabei, sich auf das Wesentliche zu konzentrieren und überflüssige Syntax zu vermeiden.
Auch versuche ich in Codebeispielen grundsätzlich Englisch zu verwenden, da dies nunmal die lingua franca in der IT-Welt ist. Wer etwas tiefer in die Materie einsteigen möchte kommt früher oder später nicht umhin sich damit zu befassen. Das zu ignorieren tut wohl niemandem einem Gefallen.
Jedenfalls nochmal vielen Dank für deine Verbesserung! :-)
Viele Grüße,
Orlok
hallo
Die Änderung am zweiten Beispiel musste ich allerdings leider rückgängig machen, denn es kann nicht unser Ziel sein Codebeispiele zu geben, die nicht mehr den aktuellen best practices entsprechen. Die Sprache entwickelt sich weiter.
document.body.addEventListener('click', function({ target }) { output.value = `click ${ main.contains(target) ? 'inside' : 'outside' } main element`; });
Ich halte es nicht für eine best Practice, Syntax zu verwenden, die immer noch relevante Browser aussschliesst und mit einem polyfill nicht zu fixen ist, vor allem dann nicht, wenn die Syntax rein ästhetischer oder rationeller Natur ist.
Da wäre mindestens ein Kommentar angebracht, wenn man das tut.
hallo
Alternativ kannst du natürlich eine oder mehrere Referenzen auf die
nav
-Elemente der Seite speichern und dann mittelsNode.contains(otherNode)
vergleichen.const nav = document.querySelector('nav'); document.body.addEventListener('click', event => nav.contains(event.target) && console.info('click in nav'))
bery cool
document.body.addEventListener("click", function(ev){
if(! nav.contains(ev.target)) nav.querySelector("details[open]").open = false;
});
Ob's dann später ein querySelectorAll sein muss, oder ich gleich mehrere Navigationen überprüfen muss, ist erst mal unwichtig.