---
description: 由 <expanding-list> 改編而來。
---

# \<dom-hierarchy> ⭐️

{% tabs %}
{% tab title="📗 參考" %}
* 👉 [\<expanding-list>](expanding-list)
* 💾 [\<dom-hierarchy>](https://replit.com/@pegasusroe/dom-hierarchy)
* [List Style Recipes](https://css-tricks.com/list-style-recipes/) - CSSTricks ⭐️
{% endtab %}

{% tab title="📘 手冊" %}
* [::marker](https://developer.mozilla.org/en-US/docs/Web/CSS/::marker) - 指定 \<li> 的「項目符號」
* [node.nodeType](https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType) - 可用於判斷一個 node 是不是 **#text** node
* [element.after()](https://developer.mozilla.org/en-US/docs/Web/API/Element/after) - 在 element 後面插入一個 node (next sibling)
{% endtab %}

{% tab title="🛠 素材" %}
* [Font Awesome](https://fontawesome.com/v5.15/icons?d=gallery\&p=2\&q=minus)
* [Black Circled Letters](http://xahlee.info/comp/unicode_circled_numbers.html)
{% endtab %}
{% endtabs %}

* live demo: [DOM Hierarchy](https://dom-hierarchy.pegasusroe.repl.co/)

{% tabs %}
{% tab title="DomHierarchy.js" %}
```javascript
/*
    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' });
```
{% endtab %}

{% tab title="html" %}
```markup
	<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>
```
{% endtab %}

{% tab title="css" %}
```css
/* 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;
}
```
{% endtab %}

{% tab title="💾 codepen" %}
{% embed url="https://codepen.io/lochiwei/pen/NWgpavB" %}


{% endtab %}
{% endtabs %}
