createDetectDirective.mjs

import isEqual from 'lodash-es/isEqual.js'
import genID from 'wsemi/src/genID.mjs'
import isfun from 'wsemi/src/isfun.mjs'
import createVnodeEmitter from './createVnodeEmitter.mjs'
import parseDetectValue from './parseDetectValue.mjs'


/**
 * 建立偵測類自訂指令(v-domresize、v-dommutation、v-domstable、v-domvisible)之共同生命週期
 *
 * 各指令只提供可用設定鍵、設定正規化與「以設定建立偵測器」, 屬性、指令值解析、重建或重新比較、事件轉發與清除皆由此處理, 使同一規則只寫一處
 * 綁定以元件實例(元件標籤上之指令)或元素(元素上之指令)識別, 兩者皆跨重繪不變: 元件標籤與其根元素為同一DOM元素, 若以元素或其屬性識別兩者會互相覆蓋(事件送錯處理函式、解除其一時清掉另一個)
 * 宿主重繪後(componentUpdated, 子節點已更新): 觸發器改用新vnode之監聽物件; 設定快照改變(啟停、設定之值、函數設定之有無)則解除舊偵測器後重建;
 * 快照未變而重新比較鍵之值改變者, 延至本批更新之後(microtask)並合併請偵測器重新比較; 其餘不動作
 * 宿主重繪不自動重新比較: 重新比較會呼叫使用端函數(如getBase), 於hook內呼叫會使其讀取之反應式狀態成為宿主之依賴, 且多實例共用比較基準時互相觸發而永不收斂
 * 函數設定一律呼叫最新指令值中之同名函數; 其拋錯時每個綁定之每個鍵以console.error提示一次, 再交回偵測器原本之處理
 * 每次建立之偵測器各自一個閘門, 解除或重建時關閉, 故舊偵測器已排定之事件不會送達
 *
 * @param {Object} def 輸入指令定義物件
 * @param {String} def.name 輸入指令與事件名稱字串, 例如'domresize'
 * @param {String} def.attr 輸入綁定時設於元素之屬性名稱字串, 例如'ev-resize'
 * @param {Array} [def.keys=[]] 輸入可用設定鍵之字串陣列
 * @param {String} [def.refreshKey=''] 輸入重新比較鍵之名稱字串, 須同時列於keys, 偵測器須提供refresh, 預設''表示無
 * @param {Function} [def.normalize=null] 輸入設定正規化函數, 傳入只含可用鍵之設定物件與{given}, 回傳{cfg,warns}, 預設null
 * @param {Function} def.create 輸入建立偵測器函數, 傳入(el, cfg, emit), 回傳{clear,refresh}(refresh可無), 或null表示未建立
 * @returns {Object} 回傳Vue 2自訂指令定義物件, 含bind、componentUpdated、unbind
 */
function createDetectDirective(def) {
    let { name, attr, create } = def
    let spec = {
        keys: def.keys || [],
        refreshKey: def.refreshKey || '',
        normalize: def.normalize || null,
    }
    let recs = new WeakMap()

    //getOwner, 綁定之識別
    let getOwner = (el, vnode) => {
        return (vnode && vnode.componentInstance) || el
    }

    //warn, 同一綁定之同一訊息只發一次(含重繪後才出現者)
    let warn = (rec, msgs) => {
        for (let m of msgs) {
            if (rec.warned.has(m)) {
                continue
            }
            rec.warned.add(m)
            console.warn(`[v-${name}] ${m}`)
        }
    }

    //start, 依rec.p建立偵測器
    let start = (el, rec) => {
        let p = rec.p
        if (!p.enabled) {
            return
        }

        //cfg, 函數設定改為呼叫最新指令值之同名函數
        let cfg = {}
        for (let k of Object.keys(p.cfg)) {
            let v = p.cfg[k]
            if (isfun(v)) {
                cfg[k] = (...args) => {
                    let f = rec.p.cfg[k]
                    if (!isfun(f)) {
                        return null
                    }
                    try {
                        return f(...args)
                    }
                    catch (err) {
                        if (!rec.warned.has(`fun:${k}`)) {
                            rec.warned.add(`fun:${k}`)
                            console.error(`[v-${name}] 設定函數${k}拋錯`, err)
                        }
                        throw err
                    }
                }
            }
            else {
                cfg[k] = v
            }
        }

        //gate, emit
        let gate = { alive: true }
        let emit = (data) => {
            if (gate.alive) {
                rec.em.emit(data)
            }
        }

        //create
        let d = create(el, cfg, emit)
        rec.det = {
            gate,
            clear: d && isfun(d.clear) ? d.clear : null,
            refresh: d && isfun(d.refresh) ? d.refresh : null,
        }

    }

    //stop, 解除偵測器
    let stop = (rec) => {
        let det = rec.det
        if (!det) {
            return
        }
        rec.det = null
        det.gate.alive = false
        if (det.clear) {
            det.clear()
        }
    }

    //refreshLater, 延至microtask並合併; 執行時偵測器已解除或重建者不動作(重建之偵測器自行量測)
    let refreshLater = (rec) => {
        let det = rec.det
        if (!det || !det.refresh || rec.refreshing) {
            return
        }
        rec.refreshing = true
        Promise.resolve().then(() => {
            rec.refreshing = false
            if (rec.det === det) {
                det.refresh()
            }
        })
    }

    //release, 作廢綁定之觸發器與偵測器
    let release = (rec) => {
        rec.em.dispose()
        stop(rec)
    }

    function bind(el, binding, vnode) {
        //console.log('bind', 'el', el, 'binding', binding, 'vnode', vnode)

        //id, 屬性保留供外部以選擇器定位
        let id = `r${genID()}`
        el.setAttribute(attr, id)

        //owner, 同一識別已有綁定(未經unbind)時先作廢, 避免舊偵測器遺留
        let owner = getOwner(el, vnode)
        let old = recs.get(owner)
        if (old) {
            release(old)
        }

        //rec
        let rec = {
            em: createVnodeEmitter(vnode, name, name),
            p: parseDetectValue(binding.value, spec),
            det: null,
            warned: new Set(),
            refreshing: false,
        }
        warn(rec, rec.p.warns)
        recs.set(owner, rec)

        //start
        start(el, rec)

    }

    function componentUpdated(el, binding, vnode) {
        //console.log('componentUpdated', 'el', el, 'binding', binding, 'vnode', vnode)

        //rec
        let rec = recs.get(getOwner(el, vnode))
        if (!rec) {
            return
        }

        //update listeners
        rec.em.update(vnode)

        //p
        let p = parseDetectValue(binding.value, spec)
        warn(rec, p.warns)
        let pOld = rec.p
        rec.p = p

        //重建
        if (p.enabled !== pOld.enabled || !isEqual(p.snap, pOld.snap)) {
            stop(rec)
            start(el, rec)
            return
        }

        //refresh
        if (p.enabled && !isEqual(p.rkey, pOld.rkey)) {
            refreshLater(rec)
        }

    }

    function unbind(el, binding, vnode) {
        //console.log('unbind', 'el', el)

        //remove attr
        el.setAttribute(attr, null)

        //rec
        let owner = getOwner(el, vnode)
        let rec = recs.get(owner)
        if (!rec) {
            return
        }
        recs.delete(owner)

        //release
        release(rec)

    }

    return {
        bind,
        componentUpdated,
        unbind,
    }
}


export default createDetectDirective