domIsStable.mjs


import get from 'lodash-es/get.js'
import isfun from './isfun.mjs'
import isEle from './isEle.mjs'
import evem from './evem.mjs'
import genPm from './genPm.mjs'
import delay from './delay.mjs'


/**
 * 前端偵測DOM元素(及其子元素)是否已穩定
 *
 * @memberOf w-component-vue
 * @param {Element} ele 輸入Element元素
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {String} [opt.mode='promise'] 輸入模式字串,可使用'promise'與'event','promise'代表一次性偵測並回傳Promise(resolve回傳當前是否穩定布林值),'event'代表持續性偵測並回傳EventEmitter,預設'promise'
 * @param {Number} [opt.tolerance=0] 輸入位置容許誤差數字,單位px,預設0表示須完全相同
 * @param {Number} [opt.timeDiff=100] 輸入位置前後比對之時間差數字,單位ms,預設100
 * @param {Number} [opt.timeDetect=50] 輸入event模式輪詢偵測間隔數字,單位ms,預設50
 * @returns {Promise|Object} 回傳物件,給予'promise'時回傳Promise(resolve回傳當前是否穩定布林值,reject回傳錯誤訊息),給予'event'時回傳EventEmitter,可使用create、on、dispose函數
 */
function domIsStable(ele, opt = {}) {

    //check ele
    if (!isEle(ele)) {
        return Promise.reject('invalid element')
    }

    //check getBoundingClientRect
    if (!isfun(ele.getBoundingClientRect)) {
        return Promise.reject('invalid element.getBoundingClientRect')
    }

    //mode
    let mode = get(opt, 'mode', '')
    if (mode !== 'promise' && mode !== 'event') {
        mode = 'promise'
    }

    //tolerance, 位置容許誤差(px)
    let tolerance = get(opt, 'tolerance', 0)

    //timeDiff, 位置前後比對的時間差(ms)
    let timeDiff = get(opt, 'timeDiff', 100)

    //timeDetect, 偵測時間差(ms)
    let timeDetect = get(opt, 'timeDetect', 50)

    //getRect: 取螢幕位置與尺寸(getBoundingClientRect的left/top會反映transform位移)
    let getRect = () => {
        let r = ele.getBoundingClientRect()
        return { x: r.left, y: r.top, w: r.width, h: r.height }
    }

    //sameRect: 兩次位置/尺寸是否在誤差內相同
    let sameRect = (a, b) => {
        return !!a && !!b &&
            Math.abs(a.x - b.x) <= tolerance &&
            Math.abs(a.y - b.y) <= tolerance &&
            Math.abs(a.w - b.w) <= tolerance &&
            Math.abs(a.h - b.h) <= tolerance
    }

    //getAnims: 取得進行中的動畫(含子樹), 不支援getAnimations時退化為空陣列
    let getAnims = () => {
        try {
            return ele.getAnimations({ subtree: true })
        }
        catch (err) {
            return []
        }
    }

    //core
    let core = async () => {

        //pm
        let pm = genPm()

        try {

            //bRect, getBoundingClientRect間隔timeDetect前後比對位置是否無變化
            let r1 = getRect()
            await delay(timeDiff)
            let r2 = getRect()
            let bRect = sameRect(r1, r2)

            //bAnim, 用getAnimations().finished確認動畫也都結束(空陣列allSettled立即過)
            await Promise.allSettled(getAnims().map((a) => a.finished))
            let bAnim = getAnims().length === 0

            //b
            let b = bRect && bAnim

            //resolve, b=true代表皆無變化才視為穩定
            pm.resolve(b)

        }
        catch (err) {
            pm.reject(err)
        }

        return pm
    }

    //corePm
    let corePm = core

    //coreEv
    let coreEv = () => {

        //ev
        let ev = evem()

        //watching
        let watching = false

        //timerCreate, timerQuery
        let timerCreate = null
        let timerQuery = null

        //stop
        let stop = false

        //observe
        let observe = (ele) => {
            let b = false

            //check
            if (!isEle(ele)) {
                return b
            }

            //observe
            try {

                let bLock = false
                let bLast = false
                timerQuery = setInterval(() => {
                    if (stop) {
                        clearInterval(timerQuery)
                        return
                    }
                    if (bLock) {
                        return
                    }
                    bLock = true
                    core()
                        .then((bNow) => {
                            if (stop) {
                                return
                            }
                            if (bLast !== bNow) {
                                bLast = bNow
                                ev.emit('stable', bNow)
                            }
                        })
                        .catch(() => {
                            // console.log(err)
                        })
                        .finally(() => {
                            bLock = false
                        })
                }, timeDetect)

                b = true
            }
            catch (err) {
                // console.log(err)
            }

            return b
        }

        //create
        let create = () => {
            timerCreate = setInterval(() => {
                watching = observe(ele)
                if (watching) {
                    clearInterval(timerCreate)
                }
            }, 50)
        }

        //dispose
        let dispose = () => {
            let b = false
            try {
                clearInterval(timerCreate)
                clearInterval(timerQuery)
                stop = true
                b = true
            }
            catch (err) {
                // console.log(err)
            }
            return b
        }

        //save
        ev.create = create
        ev.dispose = dispose

        return ev
    }

    //r
    let r = null
    if (mode === 'promise') {
        r = corePm()
    }
    else if (mode === 'event') {
        r = coreEv()
    }

    return r
}


export default domIsStable