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

JavaScript拖拽手柄调整图形尺寸时位置偏移问题

拖拽调整图形尺寸时位置偏移问题修复

问题描述

开发JavaScript应用实现拖拽手柄调整图形尺寸功能时,拖拽顶部或左侧手柄会导致图形位置偏移。相关代码如下:

HTML代码

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

CSS代码

*,
*::after,
*::before {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}
body {
    height: 100vh;
}

#container {
    width: 100%;
    height: 100%;
}

.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: -5px;
    left: 50%;
    cursor: ns-resize;
    transform: translateX(-50%);
}

.handle-bottom {
    bottom: -5px;
    left: 50%;
    cursor: ns-resize;
    transform: translateX(-50%);
}

.handle-left {
    top: 50%;
    left: -5px;
    transform: translateY(-50%);
    cursor: ew-resize;
}

.handle-right {
    top: 50%;
    right: -5px;
    transform: translateY(-50%);
    cursor: ew-resize;
}

原始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);
        };

        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";
                    shape.style.left = startX - newWidth + "px";
                } else if (handleClass.includes("top")) {
                    const newHeight = startHeight + startY - e.clientY;
                    shape.style.height = newHeight + "px";
                    shape.style.top = startY - newHeight + "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 rect = element.getBoundingClientRect();
                offsetX = e.clientX - rect.left;
                offsetY = e.clientY - rect.top;
            }
        });

        document.addEventListener("mousemove", (e) => {
            if (isDragging) {
                const newX = e.clientX - offsetX;
                const newY = e.clientY - offsetY;
                element.style.left = newX + "px";
                element.style.top = newY + "px";
            }
        });

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

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

window.addEventListener("load", () => {
    DrawJs.drawRectangle(100, 60, 0, 0, "blue");
});

错误原因

处理左侧和顶部手柄的位置计算逻辑错误:

  • 未记录图形初始的left和top位置,仅依赖鼠标起始位置计算新位置,导致图形位置与手柄拖拽动作不同步。
  • 左侧手柄的left计算使用startX - newWidth,顶部手柄的top计算使用startY - newHeight,这两个公式未关联图形原始位置,必然导致偏移。

修复方案

  1. 在startResize函数中,新增记录图形初始的startLeft和startTop值。
  2. 修改左侧和顶部手柄的位置计算逻辑,基于图形初始位置和鼠标移动差值更新位置,确保手柄拖拽时图形位置与手柄同步。

修复后的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, startLeft, startTop;

        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);
            // 新增记录图形初始的left和top位置
            startLeft = parseInt(document.defaultView.getComputedStyle(shape).left);
            startTop = parseInt(document.defaultView.getComputedStyle(shape).top);
            document.addEventListener("mousemove", resize);
            document.addEventListener("mouseup", stopResize);
        };

        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 widthDelta = startX - e.clientX;
                    const newWidth = startWidth + widthDelta;
                    shape.style.width = newWidth + "px";
                    // 基于初始left位置和鼠标移动差值更新left
                    shape.style.left = startLeft - widthDelta + "px";
                } else if (handleClass.includes("top")) {
                    const heightDelta = startY - e.clientY;
                    const newHeight = startHeight + heightDelta;
                    shape.style.height = newHeight + "px";
                    // 基于初始top位置和鼠标移动差值更新top
                    shape.style.top = startTop - heightDelta + "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 rect = element.getBoundingClientRect();
                offsetX = e.clientX - rect.left;
                offsetY = e.clientY - rect.top;
            }
        });

        document.addEventListener("mousemove", (e) => {
            if (isDragging) {
                const newX = e.clientX - offsetX;
                const newY = e.clientY - offsetY;
                element.style.left = newX + "px";
                element.style.top = newY + "px";
            }
        });

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

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

window.addEventListener("load", () => {
    DrawJs.drawRectangle(100, 60, 0, 0, "blue");
});

关键修改说明

  • 在startResize中新增startLeft和startTop,记录图形调整前的位置坐标。
  • 左侧手柄:计算宽度变化量widthDelta,新宽度为startWidth + widthDelta,同时将left设置为startLeft - widthDelta,确保图形左侧随手柄拖拽同步移动。
  • 顶部手柄:计算高度变化量heightDelta,新高度为startHeight + heightDelta,同时将top设置为startTop - heightDelta,确保图形顶部随手柄拖拽同步移动。

内容的提问来源于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 11:44:51