Hierarchical headers
by jmakeig
HTML
<section id="input">
<h2>Input</h2>
<pre>const h = new Hierarchy(null,
new Hierarchy('1'), // leaf
new Hierarchy('2',
new Hierarchy('3',
new Hierarchy('4'), // leaf
new Hierarchy('5')), // leaf
new Hierarchy('6') // leaf
),
new Hierarchy('7',
new Hierarchy('8',
new Hierarchy('9') // leaf
),
new Hierarchy('A') // leaf
),
new Hierarchy('B') // leaf
);</pre>
</section>
<section id="dynamic">
<h2>Dynamic</h2>
<div></div>
</section>
<section id="static">
<h2>Static</h2>
<div>
<h3>Row-Oriented</h3>
<table>
<tr>
<th scope="row" colspan="3">1</th>
</tr>
<tr>
<th scope="rowgroup" rowspan="3">2</th>
<th scope="rowgroup" rowspan="2">3</th>
<th scope="row">4</th>
</tr>
<tr>
<th scope="row">5</th>
</tr>
<tr>
<th scope="row" colspan="2">6</th>
</tr>
<tr>
<th scope="rowgroup" rowspan="2">7</th>
<th scope="rowgroup">8</th>
<th scope="row">9</th>
</tr>
<tr>
<th scope="row" colspan="2">A</th>
</tr>
<tr>
<th scope="row" colspan="3">B</th>
</tr>
</table>
<h3>Column-Oriented</h3>
<table>
<thead>
<tr>
<th scope="col" rowspan="3">1</th>
<th scope="colgroup" colspan="3">2</th>
<th scope="colgroup" colspan="2">7</th>
<th scope="col" rowspan="3">B</th>
</tr>
<tr>
<th scope="colgroup" colspan="2">3</th>
<th scope="col" rowspan="2">6</th>
<th scope="colgroup">8</th>
<th scope="col" rowspan="2">A</th>
</tr>
<tr>
<th scope="col">4</th>
<th scope="col">5</th>
<th scope="col">9</th>
</tr>
</thead>
</table>
</div>
</section>
CSS
body {
font-family: Helvetica, sans-serif;
}
table {
margin: 1em 0;
border-collapse: collapse;
}
th,
td {
padding: 0.5em;
border: solid 1px #ccc;
}
th,
th[scope='colgroup'],
th[scope='col'] {
text-align: center;
vertical-align: top;
}
th[scope='rowgroup'],
th[scope='row'] {
text-align: left;
}
th[scope='colgroup'],
th[scope='rowgroup'] {
background: orange;
}
th:not([scope]) {
background: red;
}
pre {
padding: 0.25em 0.5em;
border: solid 0.5px green;
font-family: 'SF Mono', 'Inconsolata', 'Consolas', monospace;
font-size: 0.65em;
line-height: 1.55;
}
JavaScript
/**
* Whether something is iterable, not including `string` instances.
*
* @param {*} item
* @param {boolean} [ignoreStrings = true]
* @return {boolean}
*/
function isIterable(item, ignoreStrings = true) {
if (!exists(item)) return false;
if ('function' === typeof item[Symbol.iterator]) {
return 'string' !== typeof item || !ignoreStrings;
}
return false;
}
/**
* Whether something is not `undefined` or `null`
*
* @param {*} item
* @return {boolean}
*/
function exists(item) {
return !('undefined' === typeof item || null === item);
}
/**
* Whether something doesn’t exist or is *not* an empty `string`
*
* @param {*} item
* @return {boolean}
*/
function isEmpty(item) {
return !exists(item) || '' === item;
}
/**
* Guarantees an interable, even if passed a non-iterable,
* except for `undefined` and `null`, which are returned as-is.
*
* @param {*} oneOrMany
* @return {Iterable|Array|null|undefined}
*/
function toIterable(oneOrMany) {
if (!exists(oneOrMany)) return oneOrMany;
if (isIterable(oneOrMany)) return oneOrMany;
return [oneOrMany];
}
/**
* Creates a `Node` instance.
*
* @param {Node|string|null|undefined} name
* @return {Node}
*/
function createElement(name) {
if (name instanceof Node) return name;
if (isEmpty(name)) return document.createDocumentFragment();
return document.createElement(String(name));
}
/**
*
* @param {Iterable|Node|string|Object} param
* @param {Node} el
* @return {Node}
*/
function applyToElement(param, el) {
if (isIterable(param)) {
for (const item of param) {
applyToElement(item, el);
}
return el;
}
if (param instanceof Node) {
el.appendChild(param);
return el;
}
if ('string' === typeof param) {
el.appendChild(document.createTextNode(param));
return el;
}
if (exists(param) && 'object' === typeof param) {
for (const p of [
...Object.getOwnPropertyNames(param),
...Object.getOwnPropertySymbols(param)
]) {
...