The Gist is a demo of of CSS selector combinators. See MDN for documentation: CSS Selectors
Usage:
content-container
. The clicked element becomes the root of the selection, and its background color becomes gray. combinators
at the left. The default combinator is the Descendant combinator (A B)
.combinator
and clicked elementPlease note: While the component tree contains both div
and span
elements, only div
elements are selected.
The Gist also demos:
The Gist is alive here
/* By Bo Ericsson, https://www.linkedin.com/in/boeric00/ */
const controlsContainer = document.querySelector('.controls-container');
const contentContainer = document.querySelector('.content-container');
let rootElemId = '';
let mainRootElemId;
let combinator;
let combinatorOperation = ' ';
function refreshSelection() {
// Clear 'selected' class from all elems
const allElems = document.querySelectorAll(`#${mainRootElemId} *`);
allElems.forEach((elem) => {
elem.classList.remove('selected');
});
// Determine selector (if no root element is selected, set class 'none', which matches
// nothing)
const selectorStr = rootElemId !== ''
? `#${rootElemId} ${combinatorOperation} div`
: '.none';
const rootElemIdStr = rootElemId !== ''
? rootElemId
: 'unselected';
const selectedElems = document.querySelectorAll(selectorStr);
selectedElems.forEach((elem) => {
elem.classList.add('selected');
});
// Update info panel
const infoStr = `Root: ${rootElemIdStr}, Combinator: ${combinator}, Selector: '${selectorStr}', Selected elements: ${selectedElems.length}`;
document.querySelector('.info').innerHTML = infoStr;
}
const combinators = {
title: 'Combinators',
selected: '',
items: [
{ name: 'Descendant combinator (A B)', operation: ' ', selected: true },
{ name: 'Child combinator (A > B)', operation: '>', selected: false },
{ name: 'General sibling combinator (A ~ B)', operation: '~', selected: false },
{ name: 'Adjacent sibling combinator (A + B)', operation: '+', selected: false },
],
};
// Combinator click handler
function onCombinatorClick() {
const elems = document.querySelectorAll('.selector');
// Clear 'chosen' class from all combinators
elems.forEach((d) => {
d.classList.remove('chosen');
});
const combinatorName = this.innerHTML;
// Test if already selected, then unselect
if (combinators.selected === combinatorName) {
// Clear combinator
// TODO: handle un-select of combinator
// combinators.selected = '';
} else {
// Add 'chosen' class to clicked combinator
this.classList.add('chosen');
combinators.selected = combinatorName;
// Update the current combinator-related variables
combinator = combinatorName;
combinatorOperation = combinators.items.find((d) => d.name === combinatorName).operation;
// Refresh the selection
refreshSelection();
}
}
// Create combinators
const combinatorTitle = document.createElement('p');
combinatorTitle.className = 'header';
combinatorTitle.innerHTML = combinators.title;
controlsContainer.appendChild(combinatorTitle);
combinators.items.forEach((d) => {
const { name, operation, selected } = d;
const item = document.createElement('p');
item.className = 'selector';
item.innerHTML = name;
item.onclick = onCombinatorClick;
controlsContainer.appendChild(item);
if (selected) {
combinator = name;
combinatorOperation = operation;
item.classList.add('chosen');
}
});
// DOM structure
const domStructure = {
type: 'div',
dir: 'column',
children: [
{
type: 'div',
dir: 'row',
children: [
{ type: 'div' },
{ type: 'span' },
{ type: 'div' },
{ type: 'div' },
],
},
{
type: 'div',
dir: 'row',
children: [
{ type: 'div' },
{ type: 'div' },
{ type: 'div' },
{ type: 'span' },
{ type: 'span' },
{ type: 'div' },
{ type: 'span' },
],
},
{
type: 'div',
dir: 'row',
children: [
{ type: 'div' },
{
type: 'div',
dir: 'row',
children: [
{ type: 'div' },
{ type: 'span' },
],
},
],
},
{
type: 'div',
dir: 'row',
children: [
{ type: 'span' },
{ type: 'div' },
{ type: 'div' },
{ type: 'span' },
{ type: 'div' },
],
},
],
};
function onElemClick(evt) {
const elems = document.querySelectorAll('*');
elems.forEach((elem) => {
elem.classList.remove('root');
});
if (rootElemId === this.id) {
rootElemId = '';
} else {
this.classList.add('root');
rootElemId = this.id;
}
// Refresh the selection
refreshSelection();
// Don't propage the event
evt.stopPropagation();
}
// Build DOM
function walkDomTree(node, parentLevel, title) {
const level = parentLevel + 1;
const { type, dir = '', children = [] } = node;
// Create a new element
const elem = document.createElement(type);
elem.className = dir;
elem.innerHTML = `${type}${title}`;
elem.id = `${type}${title}`;
elem.onclick = onElemClick;
// Iterate over the children
children.forEach((child, idx) => {
const childTitle = `${title}-${idx}`;
const childElem = walkDomTree(child, level, childTitle);
elem.appendChild(childElem);
});
// Return the element
return elem;
}
// Instantiate the DOM tree recursively
const root = walkDomTree(domStructure, -1, '-0');
// Append the root to the content container
contentContainer.appendChild(root);
mainRootElemId = root.id;
// Initial refresh
refreshSelection();
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CSS Combinator Demo</title>
<link rel="stylesheet" href="styles.css">
<script src="index.js" defer></script>
</head>
<body>
<div class="header-block">
<span class="title">CSS Selectors</span>
<span class="info"></span>
</div>
<div class="outer-container">
<div class="controls-container"></div>
<div class="content-container">content-container</div>
</div>
</body>
</html>
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 16px;
font-style: normal;
font-variant: normal;
font-weight: normal;
height: 460px;
margin: 10px;
max-height: 460px;
max-width: 920px;
padding: 10px;
outline: 1px solid lightgray;
overflow: scroll;
width: 920px;
}
h4 {
font-size: 16px;
font-weight: bold;
margin-block-start: 0px;
}
div {
font-size: 12px;
padding: 3px;
padding-top: 2px;
min-width: 60px;
min-height: 50px;
border: 1px solid lightgray;
margin: 5px;
width: fit-content;
cursor: pointer;
background-color: white;
}
span {
height: 20px;
margin: 5px;
border: 1px solid #ffca7b;
padding: 5px;
background-color: white;
}
p {
margin-block-start: 0px;
}
.outer-container {
display: flex;
flex-direction: row;
border: none;
min-height: 420px;
margin: 0px;
}
.controls-container {
min-width: 250px;
margin: 0px;
margin-right: 10px;
font-size: 14px;
padding: 0px;
border: none;
}
.content-container {
min-width: 640px;
font-size: 12px;
margin: 0px;
}
.selected {
border: 1px solid red;
}
.root {
background-color: #e7e7e7;
}
.header {
font-weight: bold;
margin-block-start: 0px;
margin-block-end: 4px;
}
.header-block {
border: none;
display: flex;
flex-direction: row;
margin: 0px;
max-height: 30px;
min-height: 30px;
/* outline: 1px solid gray; */
overflow: hidden;
padding: 0px;
line-height: 10px;
}
.selector {
margin-block-start: px;
margin-block-end: 2px;
cursor: pointer;
}
.selector:hover {
font-weight: bold;
}
.selector.chosen {
color: green;
font-weight: bold;
}
.row {
display: flex;
flex-direction: row;
}
.column {
display: flex;
flex-direction: column;
}
.title {
max-height: 25px;
border: none;
padding: 0px;
width: 250px;
font-weight: bold;
font-size: 16px;
}
.info {
max-height: 25px;
border: none;
padding: 0px;
/* outline: 1px solid red; */
width: 640px;
font-size: 13px;
/* font-weight: bold; */
margin-top: 0px;
line-height: 12px;
}