domDragPreview.mjs

import each from 'lodash-es/each.js'
import get from 'lodash-es/get.js'
import isNumber from 'lodash-es/isNumber.js'
import genID from './genID.mjs'
import isestr from './isestr.mjs'
import domRemove from './domRemove.mjs'
import domGetPointFromEvent from './domGetPointFromEvent.mjs'
import domGetAttr from './domGetAttr.mjs'
import domGetBoudRect from './domGetBoudRect.mjs'


/**
 * 前端針對DOM元素拖曳時產生其預覽(拷貝)對象
 *
 * Unit Test: {@link https://github.com/yuda-lyu/wsemi/blob/master/test/domDragPreview.test.mjs Github}
 * @memberOf wsemi
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {String} [opt.attIdentify='dragprevid'] 輸入儲存於DOM的識別欄位名稱字串,預設'dragprevid'
 * @param {Number} [opt.containerOpacity=0.4] 輸入預覽元素透明度數字,預設0.4
 * @param {String} [opt.containerBackground='white'] 輸入預覽元素背景顏色字串,預設'white'
 * @param {Number} [opt.containerBorderWidth=1] 輸入預覽元素邊框寬度數字,預設1
 * @param {String} [opt.containerBorderColor='#f26'] 輸入預覽元素邊框顏色字串,預設'#f26'
 * @returns {Object} 回傳物件,內含createPreview、updateDragPreview、pauseDragPreview、removeDragPreview、setNodeStyle、setCoverStyle、setSellStyle、setContainerStyle、clear事件
 * @example
 * need test in browser
 *
 * let pv = domDragPreview({
 *     attIdentify,
 *     containerOpacity: previewOpacity,
 *     containerBackground: previewBackground,
 *     containerBorderWidth: previewBorderWidth,
 *     containerBorderColor: previewBorderColor,
 * })
 * pv.createPreview()
 * pv.removeDragPreview()
 * pv.clear()
 *
 */
function domDragPreview(opt = {}) {
    let _attId = 'dragpreviewid'
    let _ele = null
    let _node = null
    let _cover = null
    let _shell = null
    let _container = null
    let _pause = false

    //attIdentify, 產生preview會於preview內刪除此attr, 並偵測是否有此欄位的元素存在, 藉此判斷是否需自動移除preview
    let attIdentify = get(opt, 'attIdentify', null)
    if (!isestr(attIdentify)) {
        attIdentify = 'dragprevid'
    }

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

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

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

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

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

    function cloneNode(ele, x, y) {
        //console.log('cloneNode')

        //domGetBoudRect
        let rt = domGetBoudRect(ele)
        if (!rt) {
            return null
        }

        //備份ele
        _ele = ele

        //深複製
        let nd = ele.cloneNode(true)

        //儲存資訊
        nd.tShiftX = x - rt.left
        nd.tShiftY = y - rt.top
        nd.tWidth = ele.offsetWidth
        nd.tHeight = ele.offsetHeight
        nd.tParent = ele.parentNode
        nd.setAttribute(attIdentify, '') //清除attIdentify欄位, 使全域存在唯一識別元素

        return nd
    }

    function createPreview(ele, x, y) {
        //console.log('createPreview')

        function core() {

            //複製ele
            let node = cloneNode(ele, x, y)

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

            //創建遮罩cover
            let cover = document.createElement('div')
            cover.style.position = 'absolute'
            cover.style.zIndex = 1
            cover.style.top = 0
            cover.style.left = 0
            cover.style.width = '100%'
            cover.style.height = '100%'

            //將複製的ele(node)塞入cover
            cover.appendChild(node)

            //創建殼層shell
            let shell = document.createElement('div')
            shell.style.position = 'relative'

            //將cover塞入shell
            shell.appendChild(cover)

            //創建container
            let container = document.createElement('div')
            container.setAttribute(_attId, pid)
            container.style.position = 'fixed'
            container.style.zIndex = 100000
            container.style.width = `${node.tWidth + 2 * containerBorderWidth}px`
            container.style.height = `${node.tHeight + 2 * containerBorderWidth}px`
            container.style.opacity = containerOpacity
            container.style.background = containerBackground
            container.style.border = `${containerBorderWidth}px solid ${containerBorderColor}`
            container.style.pointerEvents = 'none' //會導致游標樣式失效, 也會使子元素游標樣式失效

            //將shell塞入container
            container.appendChild(shell)

            //將container塞入body
            //node.tParent.appendChild(container) //原本ele的父層可能也有relative與fixed, 故塞入原本ele的父層內可能會出錯
            document.querySelector('body').appendChild(container)

            //儲存至全域
            _node = node
            _cover = cover
            _shell = shell
            _container = container

            //updateDragPreview
            updateDragPreview(x, y, 'createPreview')

        }

        //core, 須try catch因可能原dom例如是按鈕要自我刪除故會導致出錯
        try {
            core()
        }
        catch (err) {}

        //檢查原始元素是否存在, 若例如拖曳項目是可自我刪除的按鈕, 就有可能產生preview後原始元素被刪除, 故需跟著清除preview
        let t = setInterval(() => {
            let id = domGetAttr(_ele, attIdentify)
            let ele = document.querySelector(`[${attIdentify}='${id}']`)
            if (!ele) {
                clearInterval(t)
                removeDragPreview()
            }
        }, 500)

    }

    function updateDragPreview(x, y, from) {
        //console.log('updateDragPreview', x, y, from)

        //check
        if (!_node || !_cover || !_shell || !_container || _pause) {
            return
        }

        //update
        _container.style.top = `${y - _node.tShiftY}px`
        _container.style.left = `${x - _node.tShiftX}px`

    }

    function pauseDragPreview(b) { //主要供debug用
        //console.log('pauseDragPreview', b)
        _pause = b
    }

    function removeDragPreview() {
        //console.log('removeDragPreview')

        //domRemove
        domRemove(`[${_attId}='${pid}']`)

        //clear
        _node = null
        _cover = null
        _shell = null
        _container = null

    }

    let evMM = function(e) {
        //console.log('window mousemove', e)
        updateDragPreview(e.clientX, e.clientY, 'window mousemove')
    }
    window.addEventListener('mousemove', evMM)

    let evTM = function(e) {
        //console.log('window touchmove', e)
        let p = domGetPointFromEvent(e)
        if (p) {
            updateDragPreview(p.clientX, p.clientY, 'window touchmove')
        }
    }
    window.addEventListener('touchmove', evTM)

    function setNodeStyle(st) {
        try {
            each(st, (v, k) => {
                _node.style[k] = v
            })
        }
        catch (err) {}
    }

    function setCoverStyle(st) {
        try {
            each(st, (v, k) => {
                _cover.style[k] = v
            })
        }
        catch (err) {}
    }

    function setSellStyle(st) {
        try {
            each(st, (v, k) => {
                _shell.style[k] = v
            })
        }
        catch (err) {}
    }

    function setContainerStyle(st) {
        try {
            each(st, (v, k) => {
                _container.style[k] = v
            })
        }
        catch (err) {}
    }

    function clear() {
        window.removeEventListener('mousemove', evMM)
        window.removeEventListener('touchmove', evTM)
    }

    return {
        createPreview,
        updateDragPreview,
        pauseDragPreview,
        removeDragPreview,
        setNodeStyle,
        setCoverStyle,
        setSellStyle,
        setContainerStyle,
        clear,
    }
}


export default domDragPreview