Skip to content

Instantly share code, notes, and snippets.

@boeric
Last active May 23, 2020 19:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save boeric/780904f668c1e2f27cdac8aa011e45cb to your computer and use it in GitHub Desktop.
Save boeric/780904f668c1e2f27cdac8aa011e45cb to your computer and use it in GitHub Desktop.
CSS Combinator Demo

CSS Selector Combinator Demo

The Gist is a demo of of CSS selector combinators. See MDN for documentation: CSS Selectors

Usage:

  • Click on an element inside the content-container. The clicked element becomes the root of the selection, and its background color becomes gray.
  • Then choose one of the CSS combinators at the left. The default combinator is the Descendant combinator (A B).
  • Explore what's selected given your choice of combinator and clicked element
  • A red border indicates the element(s) selected

Please note: While the component tree contains both div and span elements, only div elements are selected.

The Gist also demos:

  • How to construct a DOM structure recursively from a nested javascript object using native DOM methods only (no external libraries are used)
  • How to add click handlers to DOM elements produced by the code
  • How to refresh the visualization after the two types of click events (combinator choice and element selection)

The Gist is alive here

<!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>
/* 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 &gt; 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();
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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment