css collapse tree
by Laurens Maneschijn
HTML
<hr>
Idea from: <a target="_blank" href="https://iamkate.com/code/tree-views/">source</a><br>
<hr>
<h2>without css:</h2>
<ul>
<li>
<details open>
<summary>Giant planets</summary>
<ul>
<li>
<details>
<summary>Gas giants</summary>
<ul>
<li>Jupiter</li>
<li>Saturn</li>
</ul>
</details>
</li>
<li>
<details open>
<summary>Ice giants</summary>
<ul>
<li>Uranus</li>
<li>Neptune</li>
</ul>
</details>
</li>
</ul>
</details>
</li>
</ul>
<h2>with css:</h2>
<ul class="tree">
<li>
<details open>
<summary>Giant planets</summary>
<ul>
<li>
<details>
<summary>Gas giants</summary>
<ul>
<li>Jupiter</li>
<li>Saturn</li>
</ul>
</details>
</li>
<li>
<details open>
<summary>Ice giants</summary>
<ul>
<li>Uranus</li>
<li>Neptune</li>
</ul>
</details>
</li>
</ul>
</details>
</li>
</ul>
<hr>
<button class="btn_toggle" data-toggledebug>toggle debug</button>
<form>
<label>
spacing: <input type="range" data-setcss="spacing" value="1.5" min="0" max="5" step="0.5">rem
</label><br>
<label>
radius: <input type="range" data-setcss="radius" value="7" min="0" max="20" step="1">px
</label><br>
<input type="reset"></form>
CSS
.tree {
--spacing: 1.5rem;
--radius: 7px;
}
/* calculations below depend on box-sizing: border-box */
.tree *::before,
.tree *::after,
.tree *,
.tree {
box-sizing: border-box;
}
.tree li {
display: block;
position: relative;
padding-left: calc(2 * var(--spacing) - var(--radius) - 2px);
}
.tree li summary {
padding-left: calc(1 * var(--radius));
}
ul.tree,
.tree ul {
margin-left: calc(var(--radius) - var(--spacing));
padding-left: 0;
}
/* vertical lines: */
.tree ul li {
border-left: 2px solid #ddd;
}
.tree ul li:last-child {
border-color: transparent;
}
/* horizontal lines: */
.tree ul li::before {
content : '';
display : block;
position : absolute;
top : calc(var(--spacing) / -2);
left : -2px;
width : calc(var(--spacing) + 2px);
height : calc(var(--spacing) + 1px);
border : solid #ddd;
border-width : 0 0 2px 2px;
}
.tree li summary {
line-height: calc(var(--spacing));
font-size: calc(0.66667 * var(--spacing));
height: calc(var(--spacing));
overflow: hidden;
}
.tree li:not(:has(summary)) {
font-size: calc(0.66667 * var(--spacing));
}
/* remove summary default styling: */
.tree summary {
display: block;
cursor: pointer;
}
.tree summary::-webkit-details-marker,
.tree summary::marker {
display: none;
}
.tree summary:focus {
outline: none;
}
.tree summary:focus-visible {
outline: 1px dotted #000;
}
/* recreate summary markers: */
.tree li::after,
.tree summary::before {
content : '';
display : block;
position : absolute;
top : calc(var(--spacing) / 2 - var(--radius));
left : calc(var(--spacing) - var(--radius) - 1px);
width : calc(2 * var(--radius));
height : calc(2 * var(--radius));
background : #ddd;
overflow : hidden;
/*
border-radius : 50%;
*/
}
/* open/close markers: */
.tree details > summary::before {
content : '+';
z-index : 1;
background : #666;
color ...
JavaScript
// https://youmightnotneedjquery.com/#on
function addEventListener(el, eventNames, eventHandler, selector) {
if (!Array.isArray(eventNames)) {
eventNames = [eventNames];
}
if (selector) {
const wrappedHandler = (e) => {
if (e.target && e.target.matches(selector)) {
eventHandler(e);
}
};
eventNames.forEach( (eventName) => {
el.addEventListener(eventName, wrappedHandler);
});
return wrappedHandler;
} else {
eventNames.forEach( (eventName) => {
el.addEventListener(eventName, eventHandler);
});
return eventHandler;
}
}
addEventListener(document.body, 'click', (e) => {
let body = document.body;
let button = e.target;
if (body.classList.contains('debug')) {
body.classList.remove('debug');
button.classList.remove('active');
} else {
body.classList.add('debug');
button.classList.add('active');
}
}, '[data-toggledebug]');
function setCss(css){
let id = 'setCss_style';
let s = document.querySelector('#'+id);
if (!s) {
s = document.createElement('style');
s.id = id;
document.head.appendChild(s);
}
s.innerText = css;
}
function updateCss(e) {
let el1 = document.querySelector('[data-setcss="spacing"]');
let el2 = document.querySelector('[data-setcss="radius"]');
setCss(`.tree { --spacing: ${el1.value}rem; --radius: ${el2.value}px; } `);
}
updateCss();
addEventListener(
document.body,
'change,:input,hover,click,drag,mousemove'.split(','),
(e) => {
updateCss(e);
e.target.setAttribute('title', e.target.value);
},
'[data-setcss]'
);