6-click/touch长按与拖动

本文最后更新于:2021年2月15日 晚上

某2020前端互助群的每周议题(四)

题目

  • 在body元素内Append 20个类名为.box的盒子

  • 鼠标活手指长按盒子元素350ms,新增类名.active,普通点击无任何变化,点击空白,移除所有.active 元素类名

  • 鼠标或手指长按盒子元素350ms(不抬起),然后滑动选框,选框范围内所有盒子元素新增类名.active背景高亮

效果如下

.box {
    display: inline-block;
    width: 100px;
    height: 100px;
    margin: 10px;
    background-color: gray;
}

.box.active {
    background-color: skyblue;
}

个人解法

思路

演示地址

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .content {
            position: relative;
        }

        .box {
            display: inline-block;
            width: 100px;
            height: 100px;
            margin: 10px;
            background-color: gray;
        }

        .box.active {
            background-color: skyblue;
        }

        /* 阻止内容被选中 */
        .unselect {
            -webkit-user-select: none;
            -moz-user-select: none;
            -khtml-user-select: none;
            -ms-user-select: none;

            /* 以下两个属性目前并未支持,写在这里为了减少风险 */
            -o-user-select: none;
            user-select: none;
        }
    </style>
</head>

<body>
    <div class="content"></div>
</body>
<script>
    let boxNum = 20; // 盒子个数
    let holdTime = 350; // click/touch激活所需时间
    let boxList = [];
    let cont = document.querySelector('.content');
    let activeFlag = false;  // 是否激活
    let mask;  // 选区遮罩
    let timerId;
    let startPointX = 0, startPointY = 0;
    let movingPointX = 0, movingPointY = 0;

    for (let i = 0; i < boxNum; i++) {
        let item = document.createElement('div');
        item.classList.add('box')
        item.classList.add('unselect')

        item.onmousedown = holdDown;
        item.onmouseup = holdUp;
        item.ontouchstart = holdDown;
        item.ontouchend = holdUp

        // item.textContent = 'test' + i
        cont.appendChild(item);
        boxList.push(item)
    }

    cont.onmousedown = mouseDown
    cont.onmouseup = mouseUp
    cont.ontouchstart = touchStart
    cont.ontouchend = touchEnd

    document.onmouseleave = mouseLeave

    // 计时激活
    function holdDown(e) {
        console.log(e)
        let startTime = new Date();
        let endTime = 0;
        timerId = setInterval(() => {
            endTime = new Date();
            console.log(endTime - startTime)
            if (endTime - startTime >= 350) {
                clearInterval(timerId);
                // alert("长按了");
                console.log("duration of the click/touch:", endTime - startTime)
                this.classList.add('active')
                activeFlag = true
            }
        }, 50)
    }
    function holdUp() {
        clearInterval(timerId);
    }


    /* mouseEvent相关处理函数 */
    function mouseDown(e) {
        startPointX = e.clientX
        startPointY = e.clientY
        console.log(startPointX, startPointY)

        // 创建遮罩层
        createMask()
        cont.onmousemove = mouseMove
    }
    function mouseMove(e) {
        if (activeFlag) {
            activeMaskArea(e.clientX, e.clientY)
        }
    }

    function mouseUp() {
        if (activeFlag) {
            activeFlag = false
            mask.remove()
        }
        cont.onmousemove = () => { }
    }

    // 鼠标移出浏览器区域后取消遮罩,因为鼠标移出浏览器后再松开触发不了mouseup 
    function mouseLeave() {
        console.log('mouse leave')
        if (activeFlag) {
            activeFlag = false
            mask.remove()
            cont.onmousemove = () => { }
        }
    }

    /* touchEvent相关处理函数 */
    function touchStart(e) {
        console.log("touch start")
        startPointX = e.touches[0].clientX
        startPointY = e.touches[0].clientY
        console.log(startPointX, startPointY)
        createMask()
        cont.ontouchmove = touchMove
    }
    function touchMove(e) {
        console.log(e)
        if (activeFlag) {
            console.log('active move')
            activeMaskArea(e.touches[0].clientX, e.touches[0].clientY)
        }
    }
    function touchEnd(e) {
        console.log('touch end')
        // console.log(e)
        if (activeFlag) {
            activeFlag = false
            mask.remove()
        }
        cont.ontouchmove = () => { }
        e.preventDefault() // 取消后续mouseEvent, 避免执行touchEvent后再执行mouseEvent
    }


    /* 通用函数 */
    function createMask() {
        // 遮罩
        mask = document.createElement('div');
        mask.style.backgroundColor = 'yellow'
        mask.style.position = 'absolute'
        mask.style.opacity = '0.3'
        cont.appendChild(mask)
    }

    function activeMaskArea(eX, eY) {
        // 遮罩层定位
        if (eX > startPointX) {  // 向左or向右 拖动
            mask.style.left = startPointX + 'px'
            mask.style.width = (eX - startPointX) + 'px'
        }
        else {                          // 左
            mask.style.left = eX + 'px'
            mask.style.width = (startPointX - eX) + 'px'
        }

        if (eY > startPointY) {    // 向上or向下 拖动
            mask.style.top = startPointY + 'px'
            mask.style.height = (eY - startPointY) + 'px'
        }
        else {                           // 上
            mask.style.top = eY + 'px'
            mask.style.height = (startPointY - eY) + 'px'
        }

        // 处理选区内元素
        boxList.forEach((e) => {
            if ((e.offsetLeft + e.offsetWidth > mask.offsetLeft)  // 遮罩左边界
                && (e.offsetLeft < mask.offsetLeft + mask.offsetWidth)   // 遮罩右边界     
                && (e.offsetTop + e.offsetHeight > mask.offsetTop)   // 遮罩上边界   
                && (e.offsetTop < mask.offsetTop + mask.offsetHeight)) {   // 遮罩下边界               
                e.classList.add('active')
            }
            else {
                e.classList.remove('active')
            }
        })
    }
</script>

</html>

参考资料

MDN Events

mouse相关

  • mousedown
  • mousemove
  • mouseup
  • mouseleave

touch相关

  • touchdown
  • touchmove
  • touchup

MDN mouseEvent

  • mouseEvent.clientX, mouseEvent.clientY

MDN touchEvent

  • touches(存放touch的数组,多点触控)

    A list of Touches for every point of contact currently touching the surface.

  • Touch.clientX, Touch.clientY

    触点相对于可见视区(visual viewport)左边沿的的X坐标

MDN 操作文档

ELement.classList

HTMLElement.offsetLeft

MDN Date

javaScript按住鼠标左键选中元素, 实现框选(Rubberband)效果

js实现鼠标拖拽多选功能 这个的实现用了jquery..

JS与CSS阻止元素被选中及清除选中的方法总结

移動端瀏覽器 :當 Touch Event 與 Mouse Event 同時存在的時候

后记

遇到的问题

  1. 刚开始时仅使用了mouseup来取消遮罩选区,其实是有bug的,因为一旦你的鼠标移出了浏览器窗口就触发不了mouseup事件了。
  2. touchEvent触发后,mouseEvent仍然会触发,也就是说部分代码会被执行两次

解决

  1. 利用mouseLeave检测鼠标是否超出区域
document.onmouseleave = mouseLeave    
function mouseLeave() {
    console.log('mouse leave')
    if (activeFlag) {
        activeFlag = false
        mask.remove()
        cont.onmousemove = () => { }
    }
}
  1. MDN如下解释

    注意: 在很多情况下,触摸事件和鼠标事件会同时被触发(目的是让没有对触摸设备优化的代码仍然可以在触摸设备上正常工作)。如果你使用了触摸事件,可以调用 event.preventDefault() 来阻止鼠标事件被触发。

    因此可以在touchEnd事件里调用上述函数来阻止mouseEvent触发


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!