You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaScript拖拽元素时偏离鼠标指针的异常问题排查

拖拽元素偏离鼠标指针的问题修复

实现了基于mousedown/mousemove/mouseup的可拖拽元素功能,但拖拽时元素会异常偏离鼠标指针。点击黑色矩形工具按钮创建矩形后,拖拽即可复现问题。


问题分析

  1. 坐标计算逻辑错误:当前代码使用元素的视口绝对位置(getBoundingClientRect().left)计算偏移,但元素是相对容器绝对定位,应该基于容器的视口位置修正坐标。
  2. 事件干扰未处理:mousedown事件未阻止冒泡和默认行为,可能触发其他元素的事件处理,干扰拖拽逻辑。
  3. 相对坐标转换缺失:拖拽时直接用视口坐标设置元素的left/top,未转换为容器内的相对坐标,导致位置偏移。

修复方案

  1. 修正偏移计算:在mousedown时,先获取容器的视口位置,将鼠标坐标转换为容器内的相对坐标后,再计算鼠标与元素的偏移量。
  2. 阻止事件干扰:在mousedown回调中添加e.preventDefault()和e.stopPropagation(),避免事件冒泡和默认行为影响。
  3. 调整拖拽坐标:拖拽时基于容器的视口位置,计算鼠标在容器内的相对坐标,减去偏移量得到元素的目标位置。

修改后的完整代码

JavaScript代码

const DrawJs = {
    selectedShape: null,
    offsetX: 0,
    offsetY: 0,
    container: null,
    resizing: false,

    init: function (containerId) {
        const container = document.querySelector(containerId);
        this.container = container;
        return container;
    },

    drawRectangle: function (width, height, x, y, color) {
        const shape = document.createElement("div");
        shape.classList.add("resizable-shape");
        shape.style.width = width + "px";
        shape.style.height = height + "px";
        shape.style.left = x + "px";
        shape.style.top = y + "px";
        shape.style.backgroundColor = color;
        this.createHandle("handle-top", shape);
        this.createHandle("handle-bottom", shape);
        this.createHandle("handle-left", shape);
        this.createHandle("handle-right", shape);
        this.draggable(shape);

        this.container.appendChild(shape);
    },
    createHandle: function (handleClass, shape) {
        const handle = document.createElement("div");
        handle.classList.add("resizable-handle", handleClass);

        let isResizing = false;
        let startX, startY, startWidth, startHeight;

        const startResize = (e) => {
            isResizing = true;
            startX = e.clientX;
            startY = e.clientY;
            startWidth = parseInt(document.defaultView.getComputedStyle(shape).width);
            startHeight = parseInt(document.defaultView.getComputedStyle(shape).height);
            document.addEventListener("mousemove", resize);
            document.addEventListener("mouseup", stopResize);
            // 阻止事件冒泡和默认行为
            e.preventDefault();
            e.stopPropagation();
        };

        const resize = (e) => {
            if (isResizing) {
                if (handleClass.includes("right")) {
                    shape.style.width = startWidth + e.clientX - startX + "px";
                } else if (handleClass.includes("bottom")) {
                    shape.style.height = startHeight + e.clientY - startY + "px";
                } else if (handleClass.includes("left")) {
                    const newWidth = startWidth + startX - e.clientX;
                    shape.style.width = newWidth + "px";
                    // 修正左侧拖拽的left计算,基于容器相对位置
                    const containerRect = this.container.getBoundingClientRect();
                    shape.style.left = (startX - newWidth - containerRect.left) + "px";
                } else if (handleClass.includes("top")) {
                    const newHeight = startHeight + startY - e.clientY;
                    shape.style.height = newHeight + "px";
                    // 修正顶部拖拽的top计算,基于容器相对位置
                    const containerRect = this.container.getBoundingClientRect();
                    shape.style.top = (startY - newHeight - containerRect.top) + "px";
                }
            }
        };

        const stopResize = () => {
            isResizing = false;
            document.removeEventListener("mousemove", resize);
            document.removeEventListener("mouseup", stopResize);
        };

        handle.addEventListener("mousedown", startResize);
        shape.appendChild(handle);
    },
    draggable: function (element) {
        let isDragging = false;
        let offsetX, offsetY;

        element.addEventListener("mousedown", (e) => {
            if (!this.resizing) {
                isDragging = true;
                const containerRect = this.container.getBoundingClientRect();
                const elementLeft = parseInt(element.style.left);
                const elementTop = parseInt(element.style.top);
                // 计算鼠标在容器内的相对坐标,再减去元素的相对位置得到偏移
                const mouseXInContainer = e.clientX - containerRect.left;
                const mouseYInContainer = e.clientY - containerRect.top;
                offsetX = mouseXInContainer - elementLeft;
                offsetY = mouseYInContainer - elementTop;
                // 阻止事件冒泡和默认行为
                e.preventDefault();
                e.stopPropagation();
            }
        });

        document.addEventListener("mousemove", (e) => {
            if (isDragging) {
                const containerRect = this.container.getBoundingClientRect();
                // 计算鼠标在容器内的相对坐标,减去偏移得到元素目标位置
                const mouseXInContainer = e.clientX - containerRect.left;
                const mouseYInContainer = e.clientY - containerRect.top;
                const newX = mouseXInContainer - offsetX;
                const newY = mouseYInContainer - offsetY;
                element.style.left = newX + "px";
                element.style.top = newY + "px";
            }
        });

        document.addEventListener("mouseup", () => {
            isDragging = false;
        });
    }
};

const container = DrawJs.init("#container");

document.getElementById("rect-tool").addEventListener("click", () => {
    DrawJs.drawRectangle(100, 60, 300, 200, "#000");
});

CSS代码(无需修改)

*,
*::after,
*::before {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
body {
    display: grid;
    place-content: center;
}
#container {
    width: calc(50vw);
    height: 200px;
    border: 1px solid #000;
    margin-top: 40px;
    position: relative;
}
#tool-wrapper {
    margin-top: 20px;
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 14px;
}
.tool {
    width: 40px;
    height: 40px;
    border: 1px solid gray;
    list-style: none;
    cursor: pointer;
    display: grid;
    place-content: center;
    border-radius: 4px;
}

#rect-tool > div {
    width: 30px;
    height: 20px;
    background: #000;
}
#circle-tool > div {
    width: 24px;
    height: 24px;
    border-radius: 50%;
    background: #000;
}
#line-tool > div {
    width: 35px;
    height: 4px;
    border-radius: 100px;
    transform: rotate(-45deg);
    background: #000;
}
#polygon-tool > div {
    width: 0;
    height: 0;
    border-top: 16px solid transparent;
    border-left: 16px solid transparent;
    border-bottom: 16px solid #000;
    border-right: 16px solid transparent;
    transform: translateY(-25%);
}

.resizable-shape {
    position: absolute;
}

.resizable-handle {
    width: 10px;
    height: 10px;
    background-color: #fff;
    position: absolute;
    cursor: pointer;
    border-radius: 50%;
    border: 1px solid #000;
    display: none;
}
.resizable-shape:hover .resizable-handle {
    display: block;
}
.handle-top {
    top: 0;
    left: 50%;
    cursor: ns-resize;
    transform: translate(-50%, -50%);
}

.handle-bottom {
    bottom: 0;
    left: 50%;
    cursor: ns-resize;
    transform: translate(-50%, 50%);
}

.handle-left {
    top: 50%;
    left: 0;
    transform: translate(-50%, -50%);
    cursor: ew-resize;
}

.handle-right {
    top: 50%;
    right: 0;
    transform: translate(50%, -50%);
    cursor: ew-resize;
}

HTML代码(无需修改)

<div id="container"></div>

<ul id="tool-wrapper">
            <li class="tool" id="rect-tool" title="rectangle">
                <div></div>
            </li>
</ul>

内容的提问来源于stack exchange,提问作者Dinesh Basnet

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 10:37:32