<dom-hierarchy> ⭐️
由 <expanding-list> 改編而來。
List Style Recipes - CSSTricks ⭐️
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' });
Last updated
Was this helpful?