domDragBarAndScroll.mjs

import get from 'lodash-es/get.js'
import isBoolean from 'lodash-es/isBoolean.js'
import isFunction from 'lodash-es/isFunction.js'
import evem from './evem.mjs'
import domCancelEvent from './domCancelEvent.mjs'


/**
 * 前端監聽指定panel與bar的DOM元素,可偵測滾輪與拖曳bar事件
 *
 * Unit Test: {@link https://github.com/yuda-lyu/wsemi/blob/master/test/domDragBarAndScroll.test.mjs Github}
 * @memberOf wsemi
 * @param {HTMLElement} panel 輸入panel元素
 * @param {HTMLElement} bar 輸入bar元素
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {Function} [getHeighRatio=()=>1] 輸入取得高度比例函數,因組件本身或內容物可能會調整尺寸, 故需由外部給予函數取得當前heighRatio,預設()=>1
 * @param {Function} [getWidthRatio=()=>1] 輸入取得寬度比例函數,因組件本身或內容物可能會調整尺寸, 故需由外部給予函數取得當前widthRatio,預設()=>1
 * @param {Boolean} [stopScrollPropagationForPanel=false] 輸入是否停用滑鼠捲動事件向上傳遞布林值,預設false
 * @param {Boolean} [stopTouchDragPropagationForPanel=false] 輸入是否停用手機拖曳事件向上傳遞布林值,預設false
 * @param {Boolean} [useTouchDragForPanel=true] 輸入是否使用手機拖曳事件布林值,預設true
 * @returns {Object} 回傳物件,可使用on與clear函數,on可監聽pressBar、dragBar、freeBar事件,clear為釋放監聽
 * @example
 * need test in browser
 *
 * //監聽dom
 * let divPanel = document.querySelector('#id_panel')
 * let divBar = document.querySelector('#id_bar')
 * let getHeighRatio = () => 0.9
 * let das = domDragBarAndScroll(divPanel, divBar, { getHeighRatio, stopScrollPropagationForPanel: true, stopTouchDragPropagationForPanel: true })
 * das.on('scrollPanel', () => {})
 * das.on('pressBar', () => {})
 * das.on('dragBar', () => {})
 * das.on('freeBar', () => {})
 *
 * //釋放監聽
 * das.clear()
 *
 */
function domDragBarAndScroll(panel, bar, opt = {}) {
    let fPanelWheel = null
    let fPanelTouchstart = null
    let fPanelTouchmove = null
    let fPanelTouchend = null
    let fBarMousedown = null
    let fBarMouseup = null
    let fBarTouchstart = null
    let fBarTouchmove = null
    let fBarTouchend = null
    let fWindowMousemove = null
    let fWindowMouseup = null
    let fWindowScroll = null
    let fWindowWheel = null
    let fDocumentScroll = null
    let fDocumentWheel = null
    let barPressing = false

    //getHeighRatio, 因組件本身或內容物可能會調整尺寸, 故需由外部給予函數取得當前heighRatio
    let getHeighRatio = get(opt, 'getHeighRatio', null)
    if (!isFunction(getHeighRatio)) {
        getHeighRatio = () => 1
    }

    //getWidthRatio, 因組件本身或內容物可能會調整尺寸, 故需由外部給予函數取得當前widthRatio
    let getWidthRatio = get(opt, 'getWidthRatio', null)
    if (!isFunction(getWidthRatio)) {
        getWidthRatio = () => 1
    }

    //stopScrollPropagationForPanel
    let stopScrollPropagationForPanel = get(opt, 'stopScrollPropagationForPanel', null)
    if (!isBoolean(stopScrollPropagationForPanel)) {
        stopScrollPropagationForPanel = false
    }

    //stopTouchDragPropagationForPanel
    let stopTouchDragPropagationForPanel = get(opt, 'stopTouchDragPropagationForPanel', null)
    if (!isBoolean(stopTouchDragPropagationForPanel)) {
        stopTouchDragPropagationForPanel = false
    }

    //useTouchDragForPanel
    let useTouchDragForPanel = get(opt, 'useTouchDragForPanel', null)
    if (!isBoolean(useTouchDragForPanel)) {
        useTouchDragForPanel = true
    }

    //ele
    let elePanel = panel
    let eleBar = bar

    //ev
    let ev = evem()

    function add() {

        //fPanelWheel
        fPanelWheel = (e) => {
            let ry = e.deltaY / Math.abs(e.deltaY)
            let rx = e.deltaX / Math.abs(e.deltaX)
            ev.emit('scrollPanel', { ratioY: ry, ratioX: rx }) //寬版頁面, 用滾輪上下捲動, 實際是傳移動距離給bar
            if (stopScrollPropagationForPanel) {
                domCancelEvent(e) //要禁止外部元素如body被滑鼠捲動
            }
        }
        elePanel.addEventListener('wheel', fPanelWheel)

        //fPanelTouchstart
        fPanelTouchstart = (e) => {
            if (useTouchDragForPanel) {

                //acy, acx
                let acy = get(e, `touches.0.clientY`, 0)
                let acx = get(e, `touches.0.clientX`, 0)

                //cy, cx
                let cy = -acy * getHeighRatio()
                let cx = -acx * getWidthRatio()

                //barPressing
                barPressing = true

                //emit
                ev.emit('pressBar', { clientY: cy, clientX: cx }) //窄版頁面, 上鎖與紀錄頁面點擊y座標, 僅紀錄第一觸擊點座標, 另需被heighRatio修正比例

                //cancel
                if (stopTouchDragPropagationForPanel) {
                    domCancelEvent(e)
                }

            }
        }
        elePanel.addEventListener('touchstart', fPanelTouchstart)

        //fPanelTouchmove
        fPanelTouchmove = (e) => {
            if (useTouchDragForPanel && barPressing) {

                //acy, acx
                let acy = get(e, `touches.0.clientY`, 0)
                let acx = get(e, `touches.0.clientX`, 0)

                // //check, 若水平向分量滑動較多, 則清空垂直向移動並結束
                // if (acx > 0 && acy > 0 && Math.abs(acx) >= Math.abs(acy)) {
                //     e.touches[0].clientY = 0 //Cannot set property clientY of #<Touch> which has only a getter
                //     return
                // }

                //cy, cx
                let cy = -acy * getHeighRatio()
                let cx = -acx * getWidthRatio()

                //emit
                ev.emit('dragBar', { clientY: cy, clientX: cx }) //窄版頁面, 用滑動距離拖曳頁面, 實際是傳移動距離給bar, 僅紀錄第一觸擊點座標, 另需被heighRatio修正比例

                //cancel
                if (stopTouchDragPropagationForPanel) {
                    domCancelEvent(e) //要禁止外部元素如body被拖曳移動畫面, 此會導致無法左右拖曳元素(導致開啟overflow-x:auto無法使用)
                }

            }
        }
        elePanel.addEventListener('touchmove', fPanelTouchmove, { passive: false }) //必須使用passive=false否則無法cancel

        //fPanelTouchend
        fPanelTouchend = (e) => {
            if (useTouchDragForPanel && barPressing) {

                //barPressing
                barPressing = false

                //emit
                ev.emit('freeBar') //窄版頁面, 解鎖

                //cancel
                if (stopTouchDragPropagationForPanel) {
                    domCancelEvent(e)
                }

            }
        }
        elePanel.addEventListener('touchend', fPanelTouchend)

        //fBarMousedown
        fBarMousedown = (e) => {
            barPressing = true
            ev.emit('pressBar', { clientY: e.clientY, clientX: e.clientX }) //寬版bar, 上鎖與紀錄點擊y座標
            //domCancelEvent(e)
        }
        eleBar.addEventListener('mousedown', fBarMousedown)

        //fBarMouseup, 可由fWindowMouseup自動解鎖, 不過若嵌入panel有攔截mouseup事件(例如popup)會導致外面window收不到mouseup事件, 故bar的mouseup事件仍需要監聽處理解鎖行為
        fBarMouseup = (e) => {
            if (barPressing) {
                barPressing = false
                ev.emit('freeBar') //窄版bar, 解鎖
                //domCancelEvent(e)
            }
        }
        eleBar.addEventListener('mouseup', fBarMouseup)

        //fBarTouchstart
        fBarTouchstart = (e) => {
            barPressing = true
            ev.emit('pressBar', { clientY: e.touches[0].clientY, clientX: e.touches[0].clientX }) //窄版bar, 上鎖與紀錄bar點擊y座標
            //domCancelEvent(e)
        }
        eleBar.addEventListener('touchstart', fBarTouchstart)

        //fBarTouchmove
        fBarTouchmove = (e) => {
            if (barPressing) {
                ev.emit('dragBar', { clientY: e.touches[0].clientY, clientX: e.touches[0].clientX }) //窄版bar, 用滑動距離拖曳bar, 實際是傳移動距離給bar
                domCancelEvent(e) //要禁止外部元素如body被拖曳移動畫面
            }
        }
        eleBar.addEventListener('touchmove', fBarTouchmove, { passive: false }) //必須使用passive=false否則無法cancel

        //fBarTouchend
        fBarTouchend = (e) => {
            if (barPressing) {
                barPressing = false
                ev.emit('freeBar') //窄版bar, 解鎖
                //domCancelEvent(e)
            }
        }
        eleBar.addEventListener('touchend', fBarTouchend)

        //fWindowMousemove
        fWindowMousemove = (e) => {
            if (barPressing) {
                ev.emit('dragBar', { clientY: e.clientY, clientX: e.clientX }) //寬版bar, 用鎖與滑動距離拖曳bar
                //domCancelEvent(e)
            }
        }
        window.addEventListener('mousemove', fWindowMousemove)

        //fWindowMouseup
        fWindowMouseup = (e) => {
            if (barPressing) {
                barPressing = false
                ev.emit('freeBar') //寬版bar, 解鎖
                if (stopScrollPropagationForPanel) {
                    domCancelEvent(e) //要禁止回傳否則會連外部body捲軸一起移動畫面
                }
            }
        }
        window.addEventListener('mouseup', fWindowMouseup)

        //fWindowScroll
        fWindowScroll = (e) => {
            // if (barPressing) {
            //     console.log('cancel window scroll')
            //     //若已經為滑鼠捲動或觸控拖曳時, 則取消傳遞至外部的scroll事件, 避免例如手機拖曳觸底時會另外呼叫window的scroll事件
            //     //此事件無法取消(cancelable=false), 即便使用passive=false
            //     domCancelEvent(e)
            // }
        }
        window.addEventListener('scroll', fWindowScroll, { passive: false })

        //fWindowWheel
        fWindowWheel = (e) => {
            // if (barPressing) {
            //     console.log('cancel window wheel')
            //     //若已經為滑鼠捲動或觸控拖曳時, 則取消傳遞至外部的scroll事件, 避免例如手機拖曳觸底時會另外呼叫window的scroll事件
            //     //此事件無法取消(cancelable=false), 即便使用passive=false
            //     domCancelEvent(e)
            // }
        }
        window.addEventListener('wheel', fWindowWheel, { passive: false })

        //fDocumentScroll
        fDocumentScroll = (e) => {
            // if (barPressing) {
            //     console.log('cancel document scroll')
            //     //若已經為滑鼠捲動或觸控拖曳時, 則取消傳遞至外部的scroll事件, 避免例如手機拖曳觸底時會另外呼叫window的scroll事件
            //     //此事件無法取消(cancelable=false), 即便使用passive=false
            //     domCancelEvent(e)
            // }
        }
        document.addEventListener('scroll', fDocumentScroll, { passive: false })

        //fDocumentWheel
        fDocumentWheel = (e) => {
            // if (barPressing) {
            //     console.log('cancel document wheel')
            //     //若已經為滑鼠捲動或觸控拖曳時, 則取消傳遞至外部的scroll事件, 避免例如手機拖曳觸底時會另外呼叫window的scroll事件
            //     //此事件無法取消(cancelable=false), 即便使用passive=false
            //     domCancelEvent(e)
            // }
        }
        document.addEventListener('wheel', fDocumentWheel, { passive: false })

    }

    function clear() {

        //elePanel remove wheel, touchstart, touchmove, touchend
        elePanel.removeEventListener('wheel', fPanelWheel)
        elePanel.removeEventListener('touchstart', fPanelTouchstart)
        elePanel.removeEventListener('touchmove', fPanelTouchmove)
        elePanel.removeEventListener('touchend', fPanelTouchend)

        //eleBar remove mousedown, mouseup, touchstart, touchmove, touchend
        eleBar.removeEventListener('mousedown', fBarMousedown)
        eleBar.removeEventListener('mouseup', fBarMouseup)
        eleBar.removeEventListener('touchstart', fBarTouchstart)
        eleBar.removeEventListener('touchmove', fBarTouchmove)
        eleBar.removeEventListener('touchend', fBarTouchend)

        //window remove mousemove, mouseup, wheel
        window.removeEventListener('mousemove', fWindowMousemove)
        window.removeEventListener('mouseup', fWindowMouseup)
        window.removeEventListener('scroll', fWindowScroll)
        window.removeEventListener('wheel', fWindowWheel)

        //document remove scroll, wheel
        document.removeEventListener('scroll', fDocumentScroll)
        document.removeEventListener('wheel', fDocumentWheel)

    }

    //add
    add()

    //save
    ev.clear = clear

    return ev
}


export default domDragBarAndScroll