<dom-hierarchy> ⭐️
由 <expanding-list> 改編而來。
List Style Recipes - CSSTricks ⭐️
::marker - 指定 <li> 的「項目符號」
node.nodeType - 可用於判斷一個 node 是不是 #text node
element.after() - 在 element 後面插入一個 node (next sibling)
live demo: DOM Hierarchy
/*
Terms used in code
-------------------
• children list: <ul> (not class="methods" or "properties")
• ancestor: <li> containing (not necessarily direct) children list
• item: <li> (with #text node as first child)
• item title: <span class="title">
*/
// customized built-in element
class DomHierarchy extends HTMLUListElement {
// connected to document
connectedCallback() {
// ⭐️ all ancestors
this.ancestors = Array
.from(this.$all('li'))
.filter(li => this._isAncestor(li));
// for each li
this.$all('li').forEach(li => {
let isAncestor = this._isAncestor(li);
let isMethod = this._isMethod(li);
let isProperty = this._isProperty(li);
let hasMethods = this._hasMethods(li);
let hasProperties = this._hasProperties(li);
// ⭐️ get item's first child
let firstChild = li.firstChild;
// ⭐️ if not `Text` node, skip.
if(firstChild.nodeType !== Node.TEXT_NODE) return;
// is #text node, get text and remove it.
let text = firstChild.textContent; // get text
firstChild.remove(); // remove node
// ⭐️ wrap text in <span> in order to
// assign style and event handlers
// item title <span>
const titleSpan = tag('span', {
textContent: text,
attributes: {'class': 'title'}
});
// prepend title <span>
li.prepend(titleSpan);
// if item has methods
if(hasMethods){
this._insertIconAfter(titleSpan, 'method');
}
// if item has properties
if(hasProperties){
this._insertIconAfter(titleSpan, 'property');
}
// if item is ancestor
if(isAncestor){
// folder is closed by default
li.attr('class', 'closed ancestor');
// toggle folder on click (on title <span>)
titleSpan.onclick = (e) => {
this._toggleFolder(li);
};
} else {
// <li> is a normal item
li.attr('class', 'item');
}
});// end: forEach(folder)
}// end: connectedCallback()
// <li> is ancestor
_isAncestor(li){
return li.$all('ul:not(.methods, .properties)').length > 0
}
// <li> is method
_isMethod(li){
return li.parentNode.classList.contains('methods');
}
// <li> is property
_isProperty(li){
return li.parentNode.classList.contains('properties');
}
// <li> has methods
_hasMethods(li){
// ⭐️ select direct children (:scope == <li> self)
return li.$(':scope > ul.methods') !== null;
}
// <li> has properties
_hasProperties(li){
// ⭐️ select direct children (:scope == <li> self)
return li.$(':scope > ul.properties') !== null;
}
// insert icon <span> after title <span>
_insertIconAfter(titleSpan, iconType){
let isProperty = iconType == 'property';
let iconText = isProperty ? '🅟' : '🅜';
let iconClass = iconType + '-icon';
let ulSelector = isProperty ? 'ul.properties' : 'ul.methods';
// new <span> for property/method-icon
let iconSpan = tag('span', {
attributes: {'class': iconClass},
textContent: iconText
});
// insert icon <span> after title <span>
titleSpan.after(iconSpan);
// properties/methods list in the item <li>
let ul = iconSpan.parentNode.$(ulSelector);
// list is closed by default
ul.classList.add('closed');
// toggle list status on click (on icon <span>)
iconSpan.onclick = (e) => {
ul.classList.toggle('closed');
};
}
// open folder
_openFolder(li){
li.classList.remove('closed');
}
// close folder
_closeFolder(li){
li.classList.add('closed');
}
// toggel folder
_toggleFolder(li){
li.classList.toggle('closed');
}
// expand all ancestors
expandAll(){
this.ancestors.forEach(li => this._openFolder(li));
}
// collapse all ancestors
collapseAll(){
this.ancestors.forEach(li => this._closeFolder(li));
}
}// end: ExpandingList
// register <expanding-list>
customElements.define('dom-hierarchy', DomHierarchy, { extends: 'ul' });
<ul is="dom-hierarchy" id="list">
<!-- ⭐️ light DOM -->
<li>EventTarget
<ul>
<li>Node
<ul class="properties">
<li>isConnected</li>
<li>childNodes</li>
<li>firstChild</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Node#properties">more ⋯</a></li>
</ul>
<ul class="methods">
<li>appendChild()</li>
<li>insertBefore()</li>
<li>cloneNode()</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Node#methods">more ⋯</a></li>
</ul>
<ul>
<li>DocumentFragment
<ul>
<li>ShadowRoot
<ul class="properties">
<li>host</li>
<li>innerHTML</li>
</ul>
</li>
</ul>
</li>
<li>Element
<ul>
<li>HTMLElement</li>
<li>SVGElement</li>
</ul>
</li>
<li>CharacterData
<ul>
<li>Text</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>DOMTokenList
<ul class="methods">
<li>add()</li>
<li>remove()</li>
<li>toggle()</li>
<li>contains()</li>
</ul>
</li>
</ul>
/* for <ul is="dom-hierarchy"> light DOM */
ul {
list-style-type: disc;
margin-left: 1.7rem;
padding-left: 0;
}
/* ---------- open/close ---------- */
.closed > ul {
display: none;
}
/* methods/properties */
ul.methods.closed,
ul.properties.closed {
display: none;
}
.closed > ul.properties,
.closed > ul.methods {
display: block;
}
.closed > ul.properties.closed,
.closed > ul.methods.closed {
display: none;
}
/* -------- hint to click -------- */
/* highlight on hover */
.ancestor > span.title:hover,
span.method-icon:hover,
span.property-icon:hover {
background-color: hsla(60, 80%, 50%, 0.3);
padding: 0 4px;
border: 1px dotted black;
border-radius: 4px;
cursor: pointer;
}
/* -------- <li> marker -------- */
/* normal item */
li.item::marker {
content: "🔸 ";
}
/* ancestor item */
li.ancestor::marker {
content: "📂 ";
}
li.ancestor.closed::marker {
content: "📁 ";
}
/* method/property <li> */
ul.methods > li::marker {
content: "➖ ";
font-size: .5rem;
}
ul.properties > li::marker {
content: normal;
color: red;
}
/* -------- icon <span> -------- */
.property-icon,
.method-icon {
color: blue;
margin-right: 4px;
font-size: .5rem;
opacity: .5;
}
Last updated