block by boeric 780904f668c1e2f27cdac8aa011e45cb

CSS Combinator Demo

Full Screen

CSS Selector Combinator Demo

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

Usage:

Please 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

index.js

/* 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();

index.html

<!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>

styles.css

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;
}