domDropFiles.mjs

import filter from 'lodash-es/filter.js'
import get from 'lodash-es/get.js'
import each from 'lodash-es/each.js'
import evem from './evem.mjs'
import domCancelEvent from './domCancelEvent.mjs'
import pmSeries from './pmSeries.mjs'
import genPm from './genPm.mjs'
import isIE from './isIE.mjs'
import getTrace from './getTrace.mjs'


function readFilePromise(entry) {
    let pm = genPm()
    try {
        entry.file(pm.resolve, pm.reject)
    }
    catch (err) {
        pm.reject(err)
    }
    return pm
}


function readEntriesPromise(directoryReader) {
    let pm = genPm()
    try {
        directoryReader.readEntries(pm.resolve, pm.reject)
    }
    catch (err) {
        pm.reject(err)
    }
    return pm
}


async function treeItem(item) {
    let r = []

    //pm
    let pm = genPm()

    async function tree(item) {
        let file = null
        if (item.isFile) {
            file = await readFilePromise(item)
        }
        r.push({
            type: item.isDirectory ? 'folder' : item.isFile ? 'file' : 'unknow',
            path: item.fullPath,
            name: item.name,
            entry: item,
            file,
        })
        if (item.isDirectory) {
            let directoryReader = item.createReader()
            let entries = await readEntriesPromise(directoryReader)
            await pmSeries(entries, async (entry) => {
                await tree(entry)
            })
        }
    }

    //tree
    await tree(item)

    //resolve
    pm.resolve(r)

    return pm
}


async function treeItems(items) {
    let rs = []
    await pmSeries(items, async (item) => {
        let r = await treeItem(item)
        each(r, (vv) => {
            rs.push(vv)
        })
    })
    return rs
}


async function treeEntries(items) {

    //its
    let its = []
    each(items, (item) => {
        let it = item.webkitGetAsEntry()
        if (it) {
            its.push(it)
        }
    })
    // console.log('its', its)

    //treeItems
    let r = await treeItems(its)

    return r
}


async function treeFiles(files) {
    let r = []
    each(files, (v) => {
        if (v.type) {
            r.push({
                type: 'file',
                name: v.name,
                file: v,
            })
        }
    })
    return r
}


async function treeDataTransfer(dataTransfer) {
    let files = []
    let entries = []

    try {

        //files, 全部瀏覽器
        files = await treeFiles(get(dataTransfer, 'files', []))
        // console.log('files', files)

        //entries, 非IE11與Opera瀏覽器
        if (!isIE()) {
            entries = await treeEntries(get(dataTransfer, 'items', []))
                .catch((err) => {
                    console.log(err) //本機瀏覽html模式時也會報錯[EncodingError]: A URI supplied to the API was malformed, or the resulting Data URL has exceeded the URL length limitations for Data URLs.
                })
            // console.log('entries', entries)
        }

    }
    catch (err) {
        return Promise.reject(err)
    }

    return {
        files,
        entries,
    }
}


/**
 * 前端取得拖曳進指定元素的檔案陣列
 *
 * Unit Test: {@link https://github.com/yuda-lyu/wsemi/blob/master/test/domDropFiles.test.mjs Github}
 * @memberOf wsemi
 * @param {HTMLElement} ele 輸入dom元素
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {Function} [opt.onTrace=null] 輸入觀察回調函數,於內部事件發生當下同步呼叫,傳入一個凍結之事件物件{ type, ... },供測試以事件(而非等待時間)同步與診斷;type為'in'時為收到dragenter、dragover或drop而即將依狀態決定是否派發dropIn(含emit為是否為自上次移出後首次移入而即將派發dropIn),'out'時為收到dragleave或呼叫getFiles之cb而暫視為移出並排定100ms後判定(含by為'dragleave'或'cb'、ms固定為100),'leave'時為100ms後之判定(含emit為期間未再移入而即將派發dropOut,false代表期間又收到in而不派發),'drop'時為收到drop事件而開始讀取拖曳內容,'files'時為拖曳內容讀取完成而即將派發getFiles(含files為dataTransfer.files中有類型者之數量、filesTree為遍歷資料夾後之檔案數量、entries為遍歷後之檔案與資料夾項目總數),'error'時為讀取失敗或getFiles之監聽器拋錯而即將派發error,'off'時為呼叫off;事件皆於派發對應事件之前通知;呼叫off之後不再通知(含off前已排定之100ms判定與讀取中者,其對ev之派發維持原行為);回調不帶接收者呼叫,其拋錯與回傳值皆忽略,不影響拖曳處理,預設null
 * @returns {Object} 回傳物件,包含emit與off事件,emit可監聽dropIn、dropOut、getFiles,通過監聽getFiles即可取得拖曳進指定元素的檔案陣列,並呼叫其內回調函數cb,方能繼續觸發dropOut事件
 * @example
 * need test in browser
 *
 * let ele = document.querySelector('#id')
 * let ev = domDropFiles(ele)
 * ev.on('getFiles', ({ files, filesTree, entries, cb }) => {
 *     console.log(files, filesTree, entries)
 *     cb()
 * })
 * ev.on('dropIn', () => {
 *     console.log('dropIn')
 * })
 * ev.on('dropOut', () => {
 *     console.log('dropOut')
 * })
  * ev.on('error', (err) => {
 *     console.log('error', err)
 * })
 *
 */
function domDropFiles(ele, opt = {}) {
    let bMouseIn = false
    let bMouseInTemp = false

    //trace, 觀察回調(opt.onTrace), 未給時為null; off後設為null使之不再通知
    let trace = getTrace(opt)

    //ev
    let ev = evem()

    //addEventListener
    ele.addEventListener('dragenter', dgDragin, false)
    ele.addEventListener('dragover', dgDragin, false)
    ele.addEventListener('dragleave', dgDragout, false)
    ele.addEventListener('drop', dgDrop, false)

    function dgIn(e) {
        domCancelEvent(e)
        bMouseInTemp = true //所有滑鼠進入或移動都標記為滑鼠移入狀態
        if (trace) {
            trace('in', { emit: bMouseIn === false })
        }
        if (bMouseIn === false) {
            bMouseIn = true
            ev.emit('dropIn', { ev: e })
        }
    }

    function dgOut(e, by) {
        domCancelEvent(e)
        bMouseInTemp = false //暫時視為滑鼠移出
        if (trace) {
            trace('out', { by, ms: 100 })
        }
        setTimeout(function() {
            if (trace) {
                trace('leave', { emit: !bMouseInTemp })
            }
            if (!bMouseInTemp) { //若100ms後仍為滑鼠移出狀態才emit事件
                bMouseIn = false
                ev.emit('dropOut', { ev: e })
            }
        }, 100)
    }

    function dgDragin(e) {
        dgIn(e)
    }

    function dgDragout(e) {
        dgOut(e, 'dragleave')
    }

    function dgDrop(e) {

        //trace
        if (trace) {
            trace('drop', {})
        }

        //dgIn
        dgIn(e)

        //cb for dgOut
        function cb() {
            dgOut(e, 'cb')
        }

        //treeDataTransfer
        treeDataTransfer(e.dataTransfer)
            .then((r) => {
                // console.log('r', r)

                //filesTree
                let filesTree = filter(r.entries, { type: 'file' })

                //trace
                if (trace) {
                    trace('files', { files: get(r, 'files.length', 0), filesTree: filesTree.length, entries: get(r, 'entries.length', 0) })
                }

                //emit
                ev.emit('getFiles', {
                    ev: e,
                    files: r.files,
                    filesTree,
                    entries: r.entries,
                    cb,
                })

            })
            .catch((err) => {
                if (trace) {
                    trace('error', {})
                }
                ev.emit('error', err)
            })

    }

    //off
    function off() {
        if (trace) {
            trace('off', {})
            trace = null //釋放後不再通知
        }
        ele.removeEventListener('dragenter', dgDragin, false)
        ele.removeEventListener('dragover', dgDragin, false)
        ele.removeEventListener('dragleave', dgDragout, false)
        ele.removeEventListener('drop', dgDrop, false)
    }

    //save
    ev.off = off

    return ev
}


export default domDropFiles