<dom-hierarchy> ⭐️

由 <expanding-list> 改編而來。

/*
    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