domDrag.mjs

import each from 'lodash-es/each.js'
import get from 'lodash-es/get.js'
import pull from 'lodash-es/pull.js'
import cloneDeep from 'lodash-es/cloneDeep.js'
import isNumber from 'lodash-es/isNumber.js'
import genID from './genID.mjs'
import isestr from './isestr.mjs'
import isEle from './isEle.mjs'
import cint from './cint.mjs'
import evem from './evem.mjs'
import haskey from './haskey.mjs'
import domGetParents from './domGetParents.mjs'
import domGetPointFromEvent from './domGetPointFromEvent.mjs'
import domElementsFromPoint from './domElementsFromPoint.mjs'
import domGetAttr from './domGetAttr.mjs'
import domGetBoudRectRefSelf from './domGetBoudRectRefSelf.mjs'
import domDragPreview from './domDragPreview.mjs'


let kpDgs = {}


function regAndGetGroupEv(gid, eid, { attIdentify, previewOpacity, previewBackground, previewBorderWidth, previewBorderColor }) {

    //create
    if (!haskey(kpDgs, gid)) {

        //evg
        let evg = evem()

        //oevg
        let funs = {
            dragMove: (msg) => {
                evg.emit(gid + '-dragMove', msg)
            },
            dragDrop: (msg) => {
                evg.emit(gid + '-dragDrop', msg)
            },
        }
        let oevg = dgEvGroup(funs)
        // console.log('oevg', oevg)

        //bind
        oevg.bind()

        //domDragPreview
        let pv = domDragPreview({
            attIdentify,
            containerOpacity: previewOpacity,
            containerBackground: previewBackground,
            containerBorderWidth: previewBorderWidth,
            containerBorderColor: previewBorderColor,
        })

        //init
        kpDgs[gid] = {
            eleIds: [],
            evg,
            oevg,
            pv,
            isInit: false,
            isActive: true,
        }

    }

    //push eid
    kpDgs[gid].eleIds.push(eid)

    //return evg
    return kpDgs[gid].evg
}


function findEleFromEventEle(e, attGroup, gid) {
    let r = null

    //domGetParents
    let ps = domGetParents(e.target)
    // console.log('ps',ps,e)

    //domGetAttr
    each(ps, (v) => {
        let attr = domGetAttr(v, attGroup)
        if (attr === gid) {
            r = v
            return false //跳出
        }
    })

    return r
}


function findEleFromEventClientXY(e, attGroup, gid) {
    let r = null

    //domGetPointFromEvent
    let p = domGetPointFromEvent(e)
    if (!p) {
        return r
    }
    // console.log('p',p)

    //domElementsFromPoint
    let ps = domElementsFromPoint(p.clientX, p.clientY)
    // console.log('ps',ps)

    //domGetAttr
    each(ps, (v) => {
        let attr = domGetAttr(v, attGroup)
        if (attr === gid) {
            r = v
            return false //跳出
        }
    })

    return r
}


function getIndex(ele, attIndex) {
    let kitem = domGetAttr(ele, attIndex)
    kitem = cint(kitem)
    return kitem
}


function dgEvGroup(funs) {
    let _window_events = []

    function bindWindowEvents() {
        let name

        name = 'mousemove'
        let fw_mousemove = function(e) {
            funs.dragMove({ e, name: 'mousemove' })
        }
        window.addEventListener(name, fw_mousemove)
        _window_events.push({ ele: window, name, f: fw_mousemove })

        name = 'mouseup'
        let fw_mouseup = function(e) {
            funs.dragDrop({ e, name: 'mouseup' })
        }
        window.addEventListener(name, fw_mouseup)
        _window_events.push({ ele: window, name, f: fw_mouseup })

        name = 'touchmove'
        let fw_touchmove = function(e) {
            funs.dragMove({ e, name: 'touchmove' })
        }
        window.addEventListener(name, fw_touchmove)
        _window_events.push({ ele: window, name, f: fw_touchmove })

        name = 'touchend'
        let fw_touchend = function(e) {
            funs.dragDrop({ e, name: 'touchend' })
        }
        window.addEventListener(name, fw_touchend)
        _window_events.push({ ele: window, name, f: fw_touchend })

    }

    function unbindWindowEvents() {
        each(_window_events, ({ ele, name, f }) => {
            ele.removeEventListener(name, f)
        })
    }

    return {
        bind: bindWindowEvents,
        unbind: unbindWindowEvents,
    }
}


function dgEvEle(ele, funs, gid, eid) {
    // console.log('dgEvEle', ele, gid, eid)
    let _ele_events = []

    function bindEleEvents() {
        let name

        name = 'mousedown'
        let fe_mousedown = function(e) {
            funs.dragStart({ e, ele, name: 'mousedown' })
        }
        ele.addEventListener(name, fe_mousedown, false)
        _ele_events.push({ ele, name, f: fe_mousedown })

        name = 'touchstart'
        let fe_touchstart = function(e) {
            funs.dragStart({ e, ele, name: 'touchstart' })
        }
        ele.addEventListener(name, fe_touchstart, false)
        _ele_events.push({ ele, name, f: fe_touchstart })

        name = 'touchmove'
        let fe_touchmove = function(e) {
            funs.dragMove({ e, name: 'touchmove' })
            //domCancelEvent(e) //不能使用domCancelEvent, 因其內使用stopPropagation會連window的touchmove無法收到訊息
            if (e.cancelable) { //window捲動中時事件為禁止取消(cancelable=false)狀態
                e.preventDefault() //必要, 需由元素touchmove事件阻止預設拖曳行為, 否則會變成捲動螢幕, 此外由window的touchmove事件來阻止會失效
            }
        }
        ele.addEventListener(name, fe_touchmove, false)
        _ele_events.push({ ele, name, f: fe_touchmove })

    }

    function unbindEleEvents() {
        each(_ele_events, ({ ele, name, f }) => {
            ele.removeEventListener(name, f)
        })
    }

    return {
        bind: bindEleEvents,
        unbind: unbindEleEvents,
    }
}


function unbindEleAndGroup(gid, eid) {

    //dg
    let dg = kpDgs[gid]

    //cloneDeep
    let eleIds = cloneDeep(dg.eleIds)

    //pull
    pull(eleIds, eid)

    //save
    dg.eleIds = eleIds

    //check
    if (eleIds.length === 0) { //全部元素已解除監聽

        //unbind
        dg.oevg.unbind()

        //clear
        dg.pv.clear()

        //delete
        delete kpDgs[gid]

    }

}


function dgDragCore({ gid, attGroup, attIndex, attIdentify, timeDragStartDelay, previewOpacity, previewDisabledOpacity }) {
    let _startInd = null
    let _startEle = null
    let _endInd = null
    let _endEle = null

    //check
    if (kpDgs[gid].isInit) {
        return kpDgs[gid].evg
    }

    //isInit
    kpDgs[gid].isInit = true

    //evg
    let evg = kpDgs[gid].evg

    //on events
    evg.on(gid + '-dragStart', dragStart)
    evg.on(gid + '-dragMove', dragMove)
    evg.on(gid + '-dragDrop', dragDrop)

    //pv
    let pv = kpDgs[gid].pv

    function dragStart({ e, ele, from }) {
        // console.log('dragStart', e, ele, from)

        //check
        if (!kpDgs[gid].isActive) {
            return
        }

        //getIndex
        let kitem = getIndex(ele, attIndex)
        // console.log('kitem', kitem)

        //check
        if (kitem === null) { //不能用!kitem判斷, 因kitem可能為0
            //console.log('dragStart: 無法取得kitem')
            return
        }

        _startInd = kitem
        _startEle = ele

        //emit
        let msg = {
            event: e,
            startInd: _startInd,
            startEle: _startEle,
            tarInd: _startInd,
            tarEid: domGetAttr(_startEle, attIdentify),
        }
        evg.emit(gid + '-change', { mode: 'start', ...msg })
        evg.emit(gid + '-start', msg)

        //p
        let p = domGetPointFromEvent(e)
        if (!p) {
            return
        }

        //setTimeout, preview不能太快出現導致原本元素例如click事件失效
        setTimeout(() => {

            //check, 若觸發例如dragDrop事件因已清除故會無_startInd
            if (_startInd === null) {
                return
            }

            //createPreview
            pv.createPreview(_startEle, p.clientX, p.clientY)

            //setContainerStyle
            pv.setContainerStyle({ opacity: previewDisabledOpacity })

        }, timeDragStartDelay)

    }

    function dragMove({ e, name, from }) {
        // console.log('dragMove', e, name, from)

        //check
        if (!kpDgs[gid].isActive) {
            return
        }

        //eleIn
        let eleIn = findEleFromEventEle(e, attGroup, gid)
        // console.log('findEleFromEventEle eleIn',eleIn)

        //check
        if (_startInd === null) {
            return
        }

        //p
        let p = domGetPointFromEvent(e)
        if (!p) {
            return
        }

        function emitEnter(endInd, endEle) {
            _endInd = endInd
            _endEle = endEle

            //emit
            let msg = {
                event: e,
                startInd: _startInd,
                startEle: _startEle,
                endInd: _endInd,
                endEle: _endEle,
                tarInd: _endInd,
                tarEid: domGetAttr(_endEle, attIdentify),
            }
            evg.emit(gid + '-change', { mode: 'enter', ...msg })
            evg.emit(gid + '-enter', msg)

            //setContainerStyle
            pv.setContainerStyle({ opacity: previewOpacity })

        }

        function emitLeave() {

            //emit
            let msg = {
                event: e,
                startInd: _startInd,
                startEle: _startEle,
                endInd: _endInd,
                endEle: _endEle,
                tarInd: _endInd,
                tarEid: domGetAttr(_endEle, attIdentify),
            }
            evg.emit(gid + '-change', { mode: 'leave', ...msg })
            evg.emit(gid + '-leave', msg)

            //clear, 要放在emit之後才能清除
            _endInd = null
            _endEle = null

            //setContainerStyle
            pv.setContainerStyle({ opacity: previewDisabledOpacity })

        }

        function emitMove(rl, rx, ry) {

            //emit
            let msg = {
                event: e,
                startInd: _startInd,
                startEle: _startEle,
                endInd: _endInd,
                endEle: _endEle,
                tarInd: _endInd,
                tarEid: domGetAttr(_endEle, attIdentify),
                ...rl,
                rx,
                ry,
            }
            evg.emit(gid + '-change', { mode: 'move', ...msg })
            evg.emit(gid + '-move', msg)

            //setContainerStyle
            pv.setContainerStyle({ opacity: previewOpacity })

        }

        //check, 滑鼠所在處的可被拖曳元素
        if (!eleIn) {

            //check
            if (_endInd !== null) {

                //emitLeave
                emitLeave()

            }

            return
        }

        //getIndex
        let kitem = getIndex(eleIn, attIndex)

        //check
        if (kitem === null) { //不能用!kitem判斷, 因kitem可能為0
            //console.log('dragMove: 無法取得kitem')
            return
        }

        //check
        if (kitem === _startInd) { //拖曳至原拖曳項目

            //由其他拖曳項目拖曳至原拖曳項目內, 需要觸發leave事件
            if (_endInd !== null) {

                //emitLeave
                emitLeave()

            }

            return
        }

        //check
        if (kitem !== _endInd) { //拖曳至不同於上一個拖曳項目
            //enter

            //於其他拖曳項目之間拖曳, 且非拖曳至原拖曳項目, 故也需要觸發leave事件
            if (_endInd !== null) {

                //emitLeave
                emitLeave()

            }

            //emitEnter
            emitEnter(kitem, eleIn)

        }
        else { //move, 於上一個拖曳項目內拖曳

            //rl
            let rl = domGetBoudRectRefSelf(p, eleIn)

            //rx, ry
            let rx = 0
            if (rl.w > 0) {
                rx = rl.x / rl.w
            }
            let ry = 0
            if (rl.h > 0) {
                ry = rl.y / rl.h
            }

            if (rx >= 0 && rx <= 1 && ry >= 0 && ry <= 1) {

                //emitMove
                emitMove(rl, rx, ry)

            }

        }

    }

    function dragDrop({ e, name, from }) {
        // console.log('dragDrop', e, name, from)

        //removeDragPreview
        pv.removeDragPreview()
        // //pauseDragPreview
        // pv.pauseDragPreview(true)

        //check
        if (!kpDgs[gid].isActive) {
            return
        }

        //check
        if (_startInd === null) {
            //console.log('dragDrop: 無_startInd')
            return
        }
        // console.log('dragDrop', '_startInd', _startInd)

        function emitDrop(endInd, endEle) {
            _endInd = endInd
            _endEle = endEle

            //emit
            let msg = {
                event: e,
                startInd: _startInd,
                startEle: _startEle,
                endInd: _endInd,
                endEle: _endEle,
                tarInd: _endInd,
                tarEid: domGetAttr(_endEle, attIdentify),
            }
            evg.emit(gid + '-change', { mode: 'drop', ...msg })
            evg.emit(gid + '-drop', msg)

        }

        //eleIn
        let eleIn = findEleFromEventClientXY(e, attGroup, gid)
        // console.log('findEleFromEventClientXY eleIn',eleIn)

        //check, 釋放時不在拖曳元素內故跳出
        if (!eleIn) {
            //console.log('dragDrop: 釋放時不在拖曳元素內')
            _startInd = null
            _startEle = null
            _endInd = null
            _endEle = null
            return
        }

        //getIndex
        let kitem = getIndex(eleIn, attIndex)

        //check
        if (kitem === null) { //不能用!kitem判斷, 因kitem可能為0
            //console.log('dragDrop: 無法取得kitem')
            return
        }

        //emitDrop, 若拖曳至原拖曳項目上也要能觸發, 否則外部收不到滑鼠放掉訊息, 僅收得到拖曳至非拖曳項目的leave事件
        emitDrop(kitem, eleIn)

        //clear, 要放在emit之後才能清除
        _startInd = null
        _startEle = null
        _endInd = null
        _endEle = null

    }

}


/**
 * 前端DOM元素拖曳事件
 *
 * Unit Test: {@link https://github.com/yuda-lyu/wsemi/blob/master/test/domDrag.test.mjs Github}
 * @memberOf wsemi
 * @param {HTMLElement} ele 輸入元素
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {String} [opt.attIdentify='dragid'] 輸入預覽元素唯一識別用之屬性名稱字串,預設'dragid'
 * @param {String} [opt.attIndex='dragindex'] 輸入預覽元素順序指標之屬性名稱字串,預設'dragindex'
 * @param {String} [opt.attGroup='draggroup'] 輸入預覽元素群組之屬性名稱字串,預設'draggroup'
 * @param {String} [opt.group='group'] 輸入預覽元素群組字串,預設'group'
 * @param {Number} [opt.timeDragStartDelay=120] 輸入預覽元素由點擊後延遲出現的時間數字,單位ms,預設120。使用pointerEvents會導致游標樣式失效,故延遲顯示可用來讓點擊事件穿透
 * @param {Number} [opt.previewOpacity=0.4] 輸入預覽元素透明度數字,預設0.4
 * @param {Number} [opt.previewDisabledOpacity=0.2] 輸入無效時(位於非可拖曳元素內)預覽元素透明度數字,預設0.2
 * @param {String} [opt.previewBackground='white'] 輸入預覽元素背景顏色字串,預設'white'
 * @param {Number} [opt.previewBorderWidth=1] 輸入預覽元素邊框寬度數字,預設1
 * @param {String} [opt.previewBorderColor='#f26'] 輸入預覽元素邊框顏色字串,預設'#f26'
 * @returns {Object} 回傳物件,可使用on與clear函數,on可監聽change、start、move、enter、leave、drop事件,clear為釋放監聽
 * @example
 * need test in browser
 *
 * //監聽dom
 * let dd = domDrag(document.querySelector('#id'), { group:'group', attIndex: 'dragindex' })
 *
 * //change
 * dd.on('change', (msg) => {
 *     console.log('change', msg)
 * })
 * dd.on('start', (msg) => {
 *     console.log('start', msg)
 * })
 * dd.on('move', (msg) => {
 *     console.log('move', msg)
 * })
 * dd.on('enter', (msg) => {
 *     console.log('enter', msg)
 * })
 * dd.on('leave', (msg) => {
 *     console.log('leave', msg)
 * })
 * dd.on('drop', (msg) => {
 *     console.log('drop', msg)
 * })
 *
 * //釋放監聽
 * dd.clear()
 *
 */
function domDrag(ele, opt = {}) {

    //check
    if (!isEle(ele)) {
        console.log('ele is not a HTMLElement', ele)
        return
    }

    //attIdentify
    let attIdentify = get(opt, 'attIdentify', null)
    if (!isestr(attIdentify)) {
        attIdentify = 'dragid'
    }

    //attIndex
    let attIndex = get(opt, 'attIndex', null)
    if (!isestr(attIndex)) {
        attIndex = 'dragindex'
    }

    //attGroup
    let attGroup = get(opt, 'attGroup', null)
    if (!isestr(attGroup)) {
        attGroup = 'draggroup'
    }

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

    //previewOpacity
    let previewOpacity = get(opt, 'previewOpacity', null)
    if (!isNumber(previewOpacity)) {
        previewOpacity = 0.4
    }

    //previewDisabledOpacity
    let previewDisabledOpacity = get(opt, 'previewDisabledOpacity', null)
    if (!isNumber(previewDisabledOpacity)) {
        previewDisabledOpacity = 0.2
    }

    //previewBackground
    let previewBackground = get(opt, 'previewBackground', null)
    if (!isestr(previewBackground)) {
        previewBackground = 'white'
    }

    //previewBorderWidth
    let previewBorderWidth = get(opt, 'previewBorderWidth', null)
    if (!isNumber(previewBorderWidth)) {
        previewBorderWidth = 1
    }

    //previewBorderColor
    let previewBorderColor = get(opt, 'previewBorderColor', null)
    if (!isestr(previewBorderColor)) {
        previewBorderColor = '#f26'
    }

    //timeDragStartDelay
    let timeDragStartDelay = get(opt, 'timeDragStartDelay', null)
    if (!isNumber(timeDragStartDelay)) {
        timeDragStartDelay = 120
    }

    //eid
    let eid = `c${genID(8)}`

    //gid
    let gid = group

    //setAttribute
    ele.setAttribute(attGroup, gid)
    ele.setAttribute(attIdentify, eid)

    //evg
    let evg = regAndGetGroupEv(gid, eid, { attIdentify, previewOpacity, previewBackground, previewBorderWidth, previewBorderColor })
    // console.log('evg', evg)

    //dgEvEle
    let eleFuncs = {
        dragStart: (msg) => {
            evg.emit(gid + '-dragStart', msg)
        },
        dragMove: (msg) => {
            evg.emit(gid + '-dragMove', msg)
        },
    }
    let oeve = dgEvEle(ele, eleFuncs, gid, eid)
    // console.log('ele', ele, 'oeve', oeve)

    //bind
    oeve.bind()

    //dgDragCore
    dgDragCore({ gid, attGroup, attIndex, attIdentify, timeDragStartDelay, previewOpacity, previewDisabledOpacity })

    function unbind() {

        //unbind
        oeve.unbind()

        //unbindEleAndGroup
        unbindEleAndGroup(gid, eid)

    }

    function setIsActive(isActive) {
        kpDgs[gid].isActive = isActive
    }

    //ev
    let ev = evem()

    //evg on
    evg.on(group + '-change', (msg) => {
        // console.log('dg evg', group + '-change', msg)
        if (msg.tarEid === eid) {
            ev.emit('change', msg)
        }
    })
    evg.on(group + '-start', (msg) => {
        // console.log('dg evg', group + '-start', msg)
        if (msg.tarEid === eid) {
            ev.emit('start', msg)
        }
    })
    evg.on(group + '-move', (msg) => {
        // console.log('dg evg', group + '-move', msg)
        if (msg.tarEid === eid) {
            ev.emit('move', msg)
        }
    })
    evg.on(group + '-enter', (msg) => {
        // console.log('dg evg', group + '-enter', msg)
        if (msg.tarEid === eid) {
            ev.emit('enter', msg)
        }
    })
    evg.on(group + '-leave', (msg) => {
        // console.log('dg evg', group + '-leave', msg)
        if (msg.tarEid === eid) {
            ev.emit('leave', msg)
        }
    })
    evg.on(group + '-drop', (msg) => {
        // console.log('dg evg', group + '-drop', msg)
        if (msg.tarEid === eid) {
            ev.emit('drop', msg)
        }
    })

    //add setIsActive
    ev.setIsActive = setIsActive

    //add unbind
    ev.unbind = unbind

    return ev
}


export default domDrag