WJsonviewTree.mjs

import './WJsonviewTree.css'


/**
 * 前端通過元素展示Json樹狀資料,因 [Fork: {@link https://github.com/pgrabovets/json-view json-view}] 沒有加入預先展開數據功能,自己下載來修改
 *
 * @export
 * @param {Object} jsonObj 輸入Json物件
 * @param {Element} rootElem 輸入初始化元素
 * @param {Object} [option={}] 輸入設定物件,預設為空物件
 * @param {Boolean} [option.expanded=false] 輸入是否預先展開,預設為false
 */
function WJsonviewTree(jsonObj, rootElem, option = {}) {


    //default expanded
    let _expanded = false


    //Init
    Init(jsonObj, rootElem, option)


    /**
     * Initialize
     */
    function Init(jsonObj, rootElem, option) {

        //get expanded
        if (option) {
            _expanded = option['expanded'] === true
        }

        //clear
        rootElem.innerHTML = ''

        //add class
        rootElem.classList.add('CompCssDJsonViewTree')

        //render
        let tree = createTree(jsonObj)
        render(tree, rootElem)

    }


    /**
     * Create html element
     * @param {String} type html element
     * @param {Object} config
     */
    function createElement(type, config) {
        let htmlElement = document.createElement(type)

        if (config === undefined) {
            return htmlElement
        }

        if (config.className) {
            htmlElement.className = config.className
        }

        if (config.content) {
            htmlElement.textContent = config.content
        }

        if (config.children) {
            config.children.forEach((el) => {
                if (el !== null) {
                    htmlElement.appendChild(el)
                }
            })
        }

        return htmlElement
    }


    /**
     * Create expanded element
     * @param {Object} node
     * @return {HTMLElement}
     */
    function createExpandedElement(node) {
        let iElem = createElement('i')

        if (node.expanded) {
            iElem.className = 'wicon w-caret-down'
        }
        else {
            iElem.className = 'wicon w-caret-right'
        }

        let caretElem = createElement('div', {
            className: 'wjv-caret-icon',
            children: [iElem],
        })

        let handleClick = node.toggle.bind(node)
        caretElem.addEventListener('click', handleClick)

        let indexElem = createElement('div', {
            className: 'wjv-json-index',
            content: node.key,
        })

        let typeElem = createElement('div', {
            className: 'wjv-json-type',
            content: node.type,
        })

        let keyElem = createElement('div', {
            className: 'wjv-json-key',
            content: node.key,
        })

        let sizeElem = createElement('div', {
            className: 'wjv-json-size'
        })

        if (node.type === 'array') {
            sizeElem.innerText = '[' + node.children.length + ']'
        }
        else if (node.type === 'object') {
            sizeElem.innerText = '{' + node.children.length + '}'
        }

        let lineChildren
        if (node.key === null) {
            lineChildren = [caretElem, typeElem, sizeElem]
        }
        else if (node.parent.type === 'array') {
            lineChildren = [caretElem, indexElem, sizeElem]
        }
        else {
            lineChildren = [caretElem, keyElem, sizeElem]
        }

        let lineElem = createElement('div', {
            className: 'wjv-line',
            children: lineChildren
        })

        if (node.depth > 0) {
            //lineElem.style = 'margin-left: ' + node.depth * 24 + 'px;' //IE11 strict模式下無法指派, style為唯讀屬性
            lineElem.setAttribute('style', 'margin-left: ' + node.depth * 24 + 'px;')
        }

        return lineElem
    }


    /**
     * Create not expanded element
     * @param {Object} node
     * @return {HTMLElement}
     */
    function createNotExpandedElement(node) {
        let caretElem = createElement('div', {
            className: 'wjv-empty-icon',
        })
        let keyElem = createElement('div', {
            className: 'wjv-json-key',
            content: node.key
        })
        let separatorElement = createElement('div', {
            className: 'wjv-json-separator',
            content: ':'
        })
        let valueType = ' wjv-json-' + typeof node.value
        let valueContent = String(node.value)
        let valueElement = createElement('div', {
            className: 'wjv-json-value' + valueType,
            content: valueContent
        })
        let lineElem = createElement('div', {
            className: 'wjv-line',
            children: [caretElem, keyElem, separatorElement, valueElement]
        })
        if (node.depth > 0) {
            //lineElem.style = 'margin-left: ' + node.depth * 24 + 'px;' //IE11 strict模式下無法指派, style為唯讀屬性
            lineElem.setAttribute('style', 'margin-left: ' + node.depth * 24 + 'px;')
        }
        return lineElem
    }


    /**
     * Create tree node
     * @return {Object}
     */
    function createNode() {
        return {
            key: null,
            parent: null,
            value: null,
            expanded: _expanded,
            type: null,
            children: null,
            elem: null,
            depth: 0,

            setCaretIconRight() {
                let icon = this.elem.querySelector('.wicon')
                icon.classList.replace('w-caret-down', 'w-caret-right')
            },

            setCaretIconDown() {
                let icon = this.elem.querySelector('.wicon')
                icon.classList.replace('w-caret-right', 'w-caret-down')
            },

            hideChildren() {
                if (this.children !== null) {
                    this.children.forEach((item) => {
                        item.elem.classList.add('wjv-json-hide')
                        if (item.expanded) {
                            item.hideChildren()
                        }
                    })
                }
            },

            showChildren() {
                if (this.children !== null) {
                    this.children.forEach((item) => {
                        item.elem.classList.remove('wjv-json-hide')
                        if (item.expanded) {
                            item.showChildren()
                        }
                    })
                }
            },

            toggle: function() {
                if (this.expanded) {
                    this.expanded = false
                    this.hideChildren()
                    this.setCaretIconRight()
                }
                else {
                    this.expanded = true
                    this.showChildren()
                    this.setCaretIconDown()
                }
            }
        }
    }


    /**
     * Return variable type
     * @param {*} val
     */
    function getType(val) {
        let type = typeof val
        if (Array.isArray(val)) {
            type = 'array'
        }
        else if (val === null) {
            type = 'null'
        }
        return type
    }


    /**
     * Recursively traverse json object
     * @param {Object} obj parsed json object
     * @param {Object} parent of object tree
     */
    function traverseObject(obj, parent) {
        for (let key in obj) {
            let child = createNode()
            child.parent = parent
            child.key = key
            child.type = getType(obj[key])
            child.depth = parent.depth + 1
            child.expanded = _expanded

            if (typeof obj[key] === 'object') {
                child.children = []
                parent.children.push(child)
                traverseObject(obj[key], child)
                child.elem = createExpandedElement(child)
            }
            else {
                child.value = obj[key]
                child.elem = createNotExpandedElement(child)
                parent.children.push(child)
            }
        }
    }


    /**
     * Create root of a tree
     * @param {Object} obj Json object
     * @return {Object}
     */
    function createTree(obj) {
        let tree = createNode()
        tree.type = getType(obj)
        tree.children = []
        tree.expanded = _expanded

        traverseObject(obj, tree)
        tree.elem = createExpandedElement(tree)

        return tree
    }


    /**
     * Recursively traverse Tree object
     * @param {Object} node
     * @param {Callback} callback
     */
    function traverseTree(node, callback) {
        callback(node)
        if (node.children !== null) {
            node.children.forEach((item) => {
                traverseTree(item, callback)
            })
        }
    }


    /**
     * Render Tree object
     * @param {Object} tree
     * @param {String} rootElem
     */
    function render(tree, rootElem) {
        traverseTree(tree, (node) => {
            if (!node.expanded) {
                node.hideChildren()
            }
            rootElem.appendChild(node.elem)
        })
    }


}


export default WJsonviewTree