Experiment details/summary

- html
- javascript
0 beatovich
0 Felix Riesterer
- javascript
- xml
0 Rolf B
0 beatovich
0 Rolf B
0 1unitedpower
0 Rolf B
0 beatovich
0 1unitedpower
0 Rolf B
0 beatovich
0 1unitedpower
0 Rolf B
hallo
Weiter unten ist eine komplette html Testseite (inclusive css/js).
Ich experimentiere mit verschachtelten detail-Elementen. Im vorliegenden Fall wird nur ein Thread in der Verschachtelung darestellt (js).
Im js gibt es aber etwas seltsames
col.forEach( function(item){
if(item === thisEl){
if( item.open == ""){
item.removeAttribute("open");
item.querySelector("summary").setAttribute("aria-expanded","true");
}
else{
item.querySelector("summary").setAttribute("aria-expanded","false");
}
}
else{
item.removeAttribute("open");
item.querySelector("summary").setAttribute("aria-expanded","false");
}
} );
richtig: wenn ich removeAttribute("open") ausführe muss ich gleichzeitig setAttribute("aria-expanded","true") anweisen.
Das ist logisch total falsch aber es 'funzt'! Zumindest auf Firefox 59
Andere Browser?
Die Frage ist zudem, ist es sinnvoll weitere aria attribute einzubetten?
Wer will kann gern einen codepen oder so von diesem Code machen. Vielleicht wird daraus ja etwas fürs Wiki.
Und ach ja: wer macht mit CSS etwas schönes daraus?
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test </title>
<style>
html{font-size:3.2mm; line-height:1.5; height:100%; }
body{ height:100%; padding: 1rem;}
*{margin:0;padding:0; font-size:1rem;box-sizing: border-box; }
:focus{outline: 2px solid orange; z-index:20; }
h1{margin: 1rem 0; font-size:2rem;}
nav { background:#fff; width:20rem; margin: 1rem 0; }
.nav { padding: 0 0 0 2rem;}
nav, .nav { /*! width:40rem; */ }
nav summary { position:relative; background:#fff; transition: background-color 1s; white-space: nowrap; padding: 0 1rem; }
nav summary[aria-expanded="true"] { background:#eee; white-space: nowrap; padding: 0 1rem; }
nav summary:focus { z-index:10; }
nav details {border:0 solid #ccc; border-width: 0 0 0.1rem 0.1rem;/*! background:#fff; */ margin-right:auto; }
nav div:not(.nav) { overflow:auto; padding: 0.5rem 1rem; /*! box-shadow: 1px 1px 2px 0px #666; */ }
nav + * { margin-top:2rem; }
</style>
</head>
<body class="noscript">
<h1>Navigation mit Details/Summary</h2>
<p>Exclusiv geschaltete detail Elemente sollen jeweils nur einen "thread" in dieser Verschachtelung anzeigen.</p>
<p>Reagiert auf Tastatur und Click</p>
<nav>
<details>
<summary>Level 1 A</summary>
<div class="nav">
<details>
<summary>Level 2 A</summary>
<div>Lorem dolores ipsum sunt </div>
</details>
<details>
<summary>Level 2 B</summary>
<div class="nav">
<details>
<summary>Level 3 A</summary>
<div>Lorem dolores ipsum sunt </div>
</details>
<details>
<summary>Level 3 B</summary>
<div>Lorem dolores ipsum sunt </div>
</details>
</div>
<div>Lorem dolores ipsum sunt </div>
</details>
</div>
</details>
<details>
<summary>Level 1 B</summary>
<div>Lorem dolores ipsum sunt </div>
</details>
<details>
<summary>Level 1 C</summary>
<div>content</div>
</details>
<details>
<summary>Level 1 D</summary>
<div>content</div>
</details>
</nav>
<p>Other Content</p>
<script>
//<!--
function init(){
document.body.classList.remove("noscript");
document.querySelectorAll(".tablist [aria-expanded], .details [aria-expanded]")
.forEach( function(item){
item.setAttribute("aria-expanded",false);
});
document.querySelectorAll("details").forEach( function(detail){
detail.querySelector("summary").setAttribute("aria-expanded","false");
detail.addEventListener(
"click",
function(ev){
ev.stopPropagation();
let thisEl=this;
console.log(thisEl.querySelector("summary"));
thisEl.querySelector("summary").setAttribute("aria-expanded","true");
var col = this.parentElement.querySelectorAll("details");
col.forEach( function(item){
if(item === thisEl){
if( item.open == ""){
item.removeAttribute("open");
item.querySelector("summary").setAttribute("aria-expanded","true");
}
else{
item.querySelector("summary").setAttribute("aria-expanded","false");
}
}
else{
item.removeAttribute("open");
item.querySelector("summary").setAttribute("aria-expanded","false");
}
} );
}
);
});
}
init();
// -->
</script>
</body>
</html>
hallo
kleine Bugfixes
//<!-- function init(){ document.body.classList.remove("noscript"); document.querySelectorAll(".tablist [aria-expanded], .details [aria-expanded]") .forEach( function(item){ item.setAttribute("aria-expanded",false); }); document.querySelectorAll("details").forEach( function(detail){ detail.querySelector("summary").setAttribute("aria-expanded","false"); detail.addEventListener( "click", function(ev){
if( ev.target.nodeName !== "SUMMARY") return;
ev.stopPropagation(); let thisEl=this; console.log(thisEl.querySelector("summary")); thisEl.querySelector("summary").setAttribute("aria-expanded","true"); var col = this.parentElement.querySelectorAll("details"); col.forEach( function(item){ if(item === thisEl){ if( item.open == ""){ item.removeAttribute("open"); item.querySelector("summary").setAttribute("aria-expanded","true"); } else{ item.querySelector("summary").setAttribute("aria-expanded","false"); } } else{ item.removeAttribute("open"); item.querySelector("summary").setAttribute("aria-expanded","false"); } } ); }
); }); }
// replace init();
document.addEventListener('DOMContentLoaded', init, false);
// --> </script>
</body> </html>
-- Neu im Forum! Signaturen kann man ausblenden!
Lieber beatovich,
//<!-- function init(){
was hat bitteschön ein HTML-Kommentar im JavaScript zu suchen? Welche (potenziell steinzeitlichen) Browser, die JavaScript nicht verstehen, möchtest Du denn noch unterstützen?
Wenn Du XML-konform JavaScript im HTML-Code unterbringen willst, dann notiere das lieber so:
<script>//<![CDATA[
function init () {}
//]]></script>
Liebe Grüße,
Felix Riesterer.
Hallo Felix und beatovich,
abgesehen davon, dass es obsolet ist, ist es falsch, auch wenn es in der MDN so drin steht.
<script>
//<!–−
alert("Hello World");
//-->
</script>
gibt auf einem Browser, der kein Script versteht, // aus. Das will man nicht, deswegen akzeptiert JavaScript im Browser <!-- als einzeiligen Kommentar[1]. Allerdings kommt der Foren-Highlighter damit nicht klar...
Für HTML muss es so aussehen:
<script>
<!–−
alert("Hello World");
//-->
</script>
Und wenn man auch XHTML richtig supporten will, dann - so sagt MDN - schreibt man diesen Schwall (und achtet im JavaScript darauf, in die Sequenz ]]> mindestens ein Space einzustreuen)
<script type="text/javascript"><!--//--><![CDATA[//><!--
...
//--><!]]></script>
Rolf
hallo
<script> <!–− alert("Hello World"); //--> </script>
Das soll einer verstehen… naja… Im Endeffekt werden externe JS Dateien eingebunden.
Aber das wesentliche Problem oder Kopfzerbrechen ist dieses offensichtlich unlogische Script, das aber zum richtigen Ergebnis führt.
if( item.open == ""){
item.removeAttribute("open");
item.querySelector("summary").setAttribute("aria-expanded","true");
}
else{
item.querySelector("summary").setAttribute("aria-expanded","false");
}
item meint hier ein details Element.
logisch wäre:
Getestet auf Firefox 59. Habe hier keinen Chorme zum Testen.
Hallo beatovich,
du hast mehrere Probleme.
Folgender Eventhandler funktioniert bei mir mit Chrome prächtig:
detail.addEventListener("toggle", function(ev) {
let thisEl = this;
this.querySelector("summary").setAttribute("aria-expanded", this.open ? "true" : "false");
if (this.open) {
var siblings = this.parentElement.querySelectorAll("details");
siblings.forEach( function(sibling) {
sibling.open = sibling === thisEl;
});
}
});
Um thisEl zu vermeiden, könnte man ev.target verwenden, oder als Callback für forEach eine Arrow-Function verwenden. Arrow-Functions erzeugen keinen neuen this-Kontext.
Ich hatte den forEach Callback zuerst als
if (sibling !== thisEl) sibling.open = false;
geschrieben, aber die blinde Zuweisung funktioniert genauso gut.
Rolf
detail.addEventListener("toggle", function(ev) { let thisEl = this; this.querySelector("summary").setAttribute("aria-expanded", this.open ? "true" : "false"); if (this.open) { var siblings = this.parentElement.querySelectorAll("details"); siblings.forEach( function(sibling) { sibling.open = sibling === thisEl; }); } });
Um thisEl zu vermeiden, könnte man ev.target verwenden
Oder einfach detail
. Wenn der Event-Handler in einem anderen Scope läge, dann wäre ev.target
auf jeden Fall this
vorzuziehen. Niemand weiß, was this
ist.
Hallo 1unitedpower,
doch, was this ist, weiß man. Innerhalb eines Event-Handlers ist es das Event-Target. Solange man da drin bleibt und keinen neuen function-Kontexte erzeugt...
Aber Du hast recht, detail wird auch funktionieren, weil die Registrierung mit einer forEach Schleife erfolgt und daher eine Closure existiert. Ich persönlich vermeide allerdings die Nutzung von Closure-Daten, wenn es sich irgendwie vermeiden lässt.
Da weiß ich allerdings nicht genau Bescheid. Sind Closures Quanten? Existieren sie, wenn sie keiner beobachtet? Sprich: Wenn der JS-JIT sieht, dass ich aus meiner Funktion nicht in den Parent-Scope hinausgreife, erzeugt er dann die Closure nicht?
Rolf
hallo
Hallo 1unitedpower,
doch, was this ist, weiß man. Innerhalb eines Event-Handlers ist es das Event-Target. Solange man da drin bleibt und keinen neuen function-Kontexte erzeugt...
in meinem Fall ist es das Element, welches den Event-Listener bekommt.
Ich habe die Vorschläge von 1unitedpower getestet. Sie waren leider nicht zielführend.
Ich werde Rolfs Hinweis mit dem toggle Event als nächsten probieren.
Ich habe die Vorschläge von 1unitedpower getestet. Sie waren leider nicht zielführend.
Dann stell uns doch mal das reduzierte Beispiel zur Verfügung.
hallo
Ich habe die Vorschläge von 1unitedpower getestet. Sie waren leider nicht zielführend.
Dann stell uns doch mal das reduzierte Beispiel zur Verfügung.
Ein reduziertes Beispiel wäre:
<body>
<details><summary>A</summary>
<p>content</p>
</details>
<details><summary>B</summary>
<div>
<details><summary>B1</summary>
<p>content</p>
</details>
<details><summary>B2</summary>
<p>content</p>
</details>
</div>
</details>
</body>
oder allgemein:
doch, was this ist, weiß man.
Kannst du die dir dynamic-scoping Relgen von JavaScript wirklich merken? Ich kann das nicht, für die folgenden Beispiele muss ich lange überlegen, was this
anstelle das Kommentars wäre. Der statische Scope ist da wesentlich einfacherer.
function click1 () { /*...*/ }
const click2 = () => { /*...*/ }
const click3 = function () { /*...*/ }
const click4 = ((...args) => click2(args))();
const click5 = function click2 () { /*...*/ }
element.addEventListener('click', click1);
element.addEventListener('click', click2);
element.addEventListener('click', click3);
element.addEventListener('click', click4);
element.addEventListener('click', click5);
element.addEventListener('click', function () { /*...*/ )});
element.addEventListener('click', () => { /*...*/ });
element.addEventListener('click', ({click: function () { /*..*/ }}).click);
element.addEventListener('click', ({click () { /*..*/ }}).click);
element.addEventListener('click', ({click : () => { /*..*/ }}).click);
Hallo 1unitedpower,
nettes Quiz 😂. Ja, ich denke, ich habe das soweit verstanden und musste eigentlich nicht überlegen. Du hast aber insofern recht, dass schon eine halbe Sekunde, die man überlegen muss, zu viel ist. Der Umgang mit this ist einer der ugly parts von JavaScript.
Aber die Regeln sind:
das Eventhandling des DOM versucht, das this des Handlers an das event-Target zu binden. Das ist HTML-DOM, nicht JavaScript. Der Rest schon.
eine function bindet ein eigenes this (je nach Aufrufart), ein Lambda bindet sich an das this, das bei seiner Definition gültig ist. Ein Lambda kann nicht mit call oder apply zwangsgebunden werden.
function foo(args) {} und foo = function(args) {} sind mMn äquivalent
Eine Eventhandler-Funktion hat deswegen event.target als this und ein Lambda nicht.
Hinter click4 steht eine IIFE mit einer Lambda-Funktion. Die ruft click2 auf, und zwar sofort. D.h. click4 enthält das, was click2 zurückgibt. Da Lambdas kein this binden, ist this in click2 das gleiche wie im Testrahmen. Was das ist - keine Ahnung. Hängt vom Rahmen ab :)
click5 ist böse Veräppelung, da wird ein Function-Objekt namens click2 in einer Variablen namens click5 gespeichert.
Also:
1: element
2: Rahmen-this
3: element
4: vermutlich sinnlos, click2 dürfte keinen Eventhandler zurückgeben
5: element
6: element
7: Rahmen-this
8: element. NICHT das Objekt, da sind schon viele drüber gestolpert
9: dito
10: Da musste ich doch überlegen. Ich denke, es ist das Rahmen-this, weil keine Objektmethode aufgerufen wird.
Der hier:
element.addEventListener('click', ({getclick: function() { return () => { /* */ }}}).getclick());
wäre was anderes, da während des getclick-Aufrufs this das literale Objekt ist und das Lambda deshalb daran gebunden ist.
Rolf
hallo
Aber Du hast recht, detail wird auch funktionieren, weil die Registrierung mit einer forEach Schleife erfolgt und daher eine Closure existiert. Ich persönlich vermeide allerdings die Nutzung von Closure-Daten, wenn es sich irgendwie vermeiden lässt.
Bin voll bei dir.
Ich habe jetzt etwas refactoriert. Das aktuell funktionierende Script:
function detailsInit(){
document.body.classList.remove("noscript");
document.querySelectorAll("details").forEach( function(detail){
detail.querySelector("summary").setAttribute("aria-expanded","false");
detail.querySelector("summary").setAttribute("role","button");
detail.addEventListener( "click", detailsInitSub );
});
}
function detailsInitSub(ev){
if( ev.target.nodeName !== "SUMMARY") return;
ev.stopPropagation();
let thisEl=this;
thisEl.querySelector("summary").setAttribute("aria-expanded","true");
this.parentElement.querySelectorAll("details").forEach( function(item){
if(item === thisEl){
if( item.open == ""){
item.removeAttribute("open");
item.querySelector("summary").setAttribute("aria-expanded","true");
}
else{
item.querySelector("summary").setAttribute("aria-expanded","false");
}
}
else{
item.removeAttribute("open");
item.querySelector("summary").setAttribute("aria-expanded","false");
}
} );
}
document.addEventListener('DOMContentLoaded', detailsInit, false);
Das ist die kritische Zeile:
item.removeAttribute("open");
Den Effekt wirst du nicht zu sehen bekommen, weil der Browser danach gleich wieder das open
-Attribut hinzufügt. Außerdem werden deine if
-Bedingungen nicht das tun, was du vermutest. Es gibt in JavaScript einen Unterschied zwischen HTML-Attributen und DOM-Properties, der hier eine Rolle spielt Und der Vergleich mit ==
verschlimmert die Sache noch. Bastle mal ein reduziertes Beispiel mit nur einem <details>
-Element. Und anschließend verfeinere das Beispiel, achte auf den Unterschied zwischen Attribut- und Properties, vermeide this
im Event-Handler und benutze nur den strikten Vergleich ===
. Dann solltest du schnell darauf kommen, was da schief läuft.
Hallo 1unitedpower,
weiß nicht, bei mir funktioniert ein item.open = false
genausogut wie ein item.removeAttribute("open")
. Der DOM Inspektor in Chrome zeigt mir, dass auch ohne „störendes“ JS ein geöffnetes details-Element ein open Attribut bekommt, das beim Schließen wieder verschwindet.
Das open Property am Elementobjekt, das bleibt stehen. Aber das wird von removeAttribute nicht entfernt. Denke ich, glaube ich, hoffe ich…
Rolf
weiß nicht, bei mir funktioniert ein
item.open = false
genausogut wie einitem.removeAttribute("open")
.
Nicht im click
-Handler. Das toggle
-Event wird erst danach ausgeführt, vermutlich hast du damit getestet. Aber so oder so, muss man weder Attribut noch Property setzen, das macht der Browser von alleine. Außer man hat einen Button außerhalb des details-Elements, der den zusätzlichen Inhalt togglen können soll. Das ist hier aber nicht der Fall.
Hallo 1unitedpower,
ich hab's im click und im toggle verwendet. Hat funktioniert. Die Spec sagt:
The open IDL attribute must reflect the open content attribute.
Und ich meine, „IDL attribute“ ist spec-Sprech für Properties der DOM-Objekte.
SETZEN (im Sinne von „auf true setzen“ muss man es als Reaktion auf click
tatsächlich nicht, aber für den gewünschten Effekt muss man es in den Geschwistern LÖSCHEN, damit die Nachbar-Äste im Baum zuklappen.
Das toggle-Event wird erst danach ausgeführt
Ja. Weil die Spec sagt, dass das Ändern des open-Zustandes einen Task in die Mikroqueue stellen muss, der die Notification übernimmt. Und zwar genau einen Task, egal wie oft open sich ändert.
Rolf
hallo
weiß nicht, bei mir funktioniert ein
item.open = false
genausogut wie einitem.removeAttribute("open")
.Nicht im
click
-Handler. Dastoggle
-Event wird erst danach ausgeführt, vermutlich hast du damit getestet. Aber so oder so, muss man weder Attribut noch Property setzen, das macht der Browser von alleine. Außer man hat einen Button außerhalb des details-Elements, der den zusätzlichen Inhalt togglen können soll. Das ist hier aber nicht der Fall.
Ihr habt natürlich nicht meinen konkreten Fall (html) getestst
Die Absicht ist:
Meine Version gewährleistet das.
Aber eventuell tritt das ein, was 1unitedpower sagte, dass nämlich click vor dem toggle gefeuert wird. Es arbeiten also 2 events nacheinander, aber nur einer wird benutzt, um die aria Attribute umzuschreiben.
Damit würde für mich die scheinbare unlogik im Code verständlich.
Hallo beatovich,
ich habe hiermit getestet: https://jsfiddle.net/Rolf_b/o1w4dzLr/
Rolf