composeToTree.mjs

import get from 'lodash-es/get.js'
import each from 'lodash-es/each.js'
import set from 'lodash-es/set.js'
import cloneDeep from 'lodash-es/cloneDeep.js'
import omit from 'lodash-es/omit.js'
import isestr from './isestr.mjs'
import isearr from './isearr.mjs'
import isbol from './isbol.mjs'
import iser from './iser.mjs'


/**
 * 組合關聯陣列成為樹狀陣列
 *
 * Unit Test: {@link https://github.com/yuda-lyu/wsemi/blob/master/test/composeToTree.test.mjs Github}
 * @memberOf wsemi
 * @param {Array} items 輸入關聯陣列
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {String} [bindKey='id'] 輸入項目物件識別用欄位字串,預設'id'
 * @param {String} [bindParent='parentId'] 輸入項目物件內存放父節點欄位字串,預設'parentId'
 * @param {String} [bindChildren='children'] 輸入產生樹狀物件時,各節點內存放子節點欄位字串,預設'children'
 * @param {Boolean} [saveExtProps=false] 輸入是否儲存項目物件從屬資訊布林值,預設false
 * @returns {Array} 回傳樹狀陣列
 * @example
 *
 * let data = [
 *     {
 *         id: 1,
 *         text: '1-a',
 *     },
 *     {
 *         id: 2,
 *         text: '2-b',
 *     },
 *     {
 *         id: 3,
 *         text: '3-c',
 *         parentId: 2,
 *     },
 *     {
 *         id: 4,
 *         text: '4-d',
 *         parentId: 2,
 *     },
 *     {
 *         id: 5,
 *         text: '5-e',
 *         parentId: 3,
 *     },
 *     {
 *         id: 6,
 *         text: 'empty',
 *     },
 * ]
 * let r = composeToTree(data)
 * let cr = JSON.stringify(r)
 * console.log(cr)
 * // => [
 * //   {
 * //     "id": 1,
 * //     "text": "1-a"
 * //   },
 * //   {
 * //     "id": 2,
 * //     "text": "2-b",
 * //     "children": [
 * //       {
 * //         "id": 3,
 * //         "text": "3-c",
 * //         "parentId": 2,
 * //         "children": [
 * //           {
 * //             "id": 5,
 * //             "text": "5-e",
 * //             "parentId": 3
 * //           }
 * //         ]
 * //       },
 * //       {
 * //         "id": 4,
 * //         "text": "4-d",
 * //         "parentId": 2
 * //       }
 * //     ]
 * //   },
 * //   {
 * //     "id": 6,
 * //     "text": "empty"
 * //   }
 * // ]
 *
 */
function composeToTree(items, opt = {}) {
    let privateLevel = '$level'
    let privateParents = '$parents'

    //check
    if (!isearr(items)) {
        return []
    }

    //bindKey
    let bindKey = get(opt, 'bindKey', null)
    if (!isestr(bindKey)) {
        bindKey = 'id'
    }

    //bindParent
    let bindParent = get(opt, 'bindParent', null)
    if (!isestr(bindParent)) {
        bindParent = 'parentId'
    }

    //bindChildren
    let bindChildren = get(opt, 'bindChildren', null)
    if (!isestr(bindChildren)) {
        bindChildren = 'children'
    }

    //saveExtProps
    let saveExtProps = get(opt, 'saveExtProps', null)
    if (!isbol(saveExtProps)) {
        saveExtProps = false
    }

    function addNodeLevel(items) {
        let r = []

        //cloneDeep
        items = cloneDeep(items)

        function addNodeForTop() {
            let _items = []

            each(items, (v, k) => {
                if (iser(v[bindParent])) {
                    //若不存在bindParent就代表頂層, 加入至r
                    r.push({
                        [privateLevel]: 0,
                        [privateParents]: [],
                        ...v,
                    })
                }
                else {
                    //非頂層項目物件, 加入至_items
                    _items.push(v)
                }
            })
            // console.log('_items a', _items)
            // console.log('r a', r)

            //re-save
            items = _items

        }

        function addNodeInParent(p) {
            let _items = []

            each(items, (v, k) => {
                let vbp = get(v, bindParent)
                let pbk = get(p, bindKey)
                if (vbp && pbk && (vbp === pbk)) {
                    //若非頂層項目物件v的bindParent, 等於所傳入的父層物件p的bindKey, 就代表找到從屬, 加入至r

                    //parentIDs
                    let parentIDs = get(p, privateParents, [])
                    parentIDs = cloneDeep(parentIDs)
                    parentIDs.push(p[bindKey])

                    //push
                    r.push({
                        [privateLevel]: p[privateLevel] + 1, //level加1
                        [privateParents]: parentIDs, //儲存父層節點的bindKey
                        ...v,
                    })

                }
                else {
                    //非頂層項目物件v非隸屬於父層物件p, 加入至_items
                    _items.push(v)
                }
            })
            // console.log('_items b', _items)
            // console.log('r b', r)

            //re-save
            items = _items

        }

        //addNodeForTop
        addNodeForTop()

        //check
        if (r.length === 0) {
            return r
        }

        //建立非頂層節點關聯
        let i = -1
        while (true) {
            i += 1

            //check, 存取超過陣列長度
            if (i > r.length - 1) {
                break
            }

            //p
            let p = r[i]

            //addNodeInParent
            addNodeInParent(p)

            //check, 若已無非頂層物件則跳出
            if (items.length === 0) {
                break
            }

        }

        return r
    }

    function buildTree(items) {
        let r = []

        function findParent(v) {
            for (let i = 0; i < items.length; i++) {
                let p = items[i]
                if (v[bindParent] === p[bindKey]) {
                    return p
                }
            }
            return null
        }

        function omitProps(v) {
            if (saveExtProps) {
                return v
            }
            return omit(v, [privateLevel, privateParents])
        }

        //填入子節點至keyChildren欄位
        let kp = {}
        each(items, (v, k) => { //r為依照層級高低循序建立
            if (v[privateLevel] === 0) {

                //omitProps
                let omv = omitProps(v)

                //push
                r.push(omv)

                //save kp
                kp[v[bindKey]] = [k] //頂層節點之指標k為keys內容
                // console.log(k, 'r', cloneDeep(r))

            }
            else {
                // console.log(k, 'v', cloneDeep(v))

                //findParent
                let p = findParent(v)
                // console.log(k, 'p', cloneDeep(p))

                //check, 找得到父層節點才處理
                if (p !== null) {

                    //keys
                    let keys = kp[p[bindKey]]
                    keys = [...keys, bindChildren]
                    // console.log(k, 'keys', cloneDeep(keys))

                    let cs = get(r, keys, [])
                    // console.log(k, 'cs1', cloneDeep(cs))

                    //omitProps
                    let omv = omitProps(v)

                    //push
                    cs.push(omv)

                    //save kp
                    kp[v[bindKey]] = [...keys, cs.length - 1]
                    // console.log(k, 'cs2', cloneDeep(cs))

                    //set
                    set(r, keys, cs)
                    // console.log(k, 'r', cloneDeep(r))

                }
                else {
                    console.log(`can not find parent for ${get(v, bindParent)}`)
                }

            }
        })

        return r
    }

    //addNodeLevel
    items = addNodeLevel(items)

    //buildTree
    let r = buildTree(items)

    return r
}


export default composeToTree