本文详解如何使用原生JavaScript实现一个可拖拽的模态框。通过监听mousedown、mousemove和mouseup事件,结合offsetLeft/Top与pageX/pageY计算坐标差值,实现模态框跟随鼠标移动。文章重点解析为何将移动事件绑定至document而非标题元素,以避免快速拖动时鼠标脱离目标导致交互失效的问题,并提供完整HTML/CSS/JS代码参考。
前言
在HTML网页环境中,并不存在传统意义上的“父窗口”与“子窗口”层级概念。此处所指的模态框,是通过一个可隐藏的遮罩层(Mask)与一个可隐藏的容器盒子(Box)组合而成的视觉效果。
效果演示:

接下来将详细介绍实现可拖拽模态框的具体步骤。本文重点解析 JavaScript 逻辑部分,HTML 结构与 CSS 样式代码将统一放置在文章底部的源代码区域中。
JavaScript详解
整体交互效果由以下几个核心事件共同构成:
- 点击“立即登录”按钮,触发遮罩层和模态框的显示。
- 点击关闭按钮(小叉号),隐藏模态框和遮罩层。
- 当鼠标在模态框标题区域按下时,计算鼠标相对于模态框内部的坐标位置。
- 为整个 HTML 文档绑定鼠标移动事件,通过特定算法使模态框跟随鼠标位置移动。
- 当鼠标在 HTML 文档区域内松开时,移除文档上的鼠标移动事件监听器。
首先,获取需要操作的 DOM 元素对象。
let but = document.querySelector('.but') // 立即登录按钮
let shade = document.querySelector('.shade') // 遮罩层
let loginBox = document.querySelector('.login-box') // 模态框
let title = document.querySelector('.title') // 模态框标题:用户登录
let exit = document.querySelector('.exit'); // 小叉号点击“立即登录”按钮,触发遮罩层和模态框的显示。
but.addEventListener('click', function() {
shade.style.display = "block"; // 显示遮罩层
loginBox.style.display = "block"; // 显示模态框
});点击关闭按钮(小叉号),隐藏模态框和遮罩层。
exit.addEventListener('click', function() {
shade.style.display = "none"; // 隐藏遮罩层
loginBox.style.display = "none"; // 隐藏遮罩层
});当鼠标在模态框标题区域按下时,计算鼠标相对于模态框内部的坐标位置:
title.addEventListener('mousedown', function(event) {
let x = event.pageX - loginBox.offsetLeft;
let y = event.pageY - loginBox.offsetTop;
});event.pageX 和 event.pageY:用于获取鼠标在整个页面视口中的 x 轴和 y 轴坐标。
loginBox.offsetLeft 和 loginBox.offsetTop:用于获取模态框容器距离页面左侧和顶部的偏移距离。
通过上述坐标相减,计算出鼠标点击点相对于模态框左上角的内部坐标。
在鼠标于模态框标题区域按下后,为 document 对象绑定鼠标移动事件:
title.addEventListener('mousedown', function(event) {
let x = event.pageX - loginBox.offsetLeft;
let y = event.pageY - loginBox.offsetTop;
document.addEventListener('mousemove', function() {
// 鼠标移动后的新坐标减去鼠标在模态框中的坐标,实现模态框跟随鼠标移动
loginBox.style.left = (event.pageX - x)+"px";
loginBox.style.top = (event.pageY - y)+"px";
});
});这里为什么不把鼠标移动事件直接绑定给 title 元素?
如果将鼠标移动事件仅绑定给 title 元素,当鼠标移动速度过快时,光标容易脱离 title 元素的覆盖范围。一旦脱离,模态框将失去移动跟随的目标,导致拖拽功能失效。
若需验证此现象,可将代码中的 document 替换为 title,并尝试快速拖动模态框,此处不再进行演示。
至此已实现模态框跟随鼠标移动的功能,但发现松开鼠标后,模态框仍可能继续移动。因此,还需要为 document 绑定鼠标松开(mouseup)事件,以终止移动逻辑。
title.addEventListener('mousedown', function(event) {
let x = event.pageX - loginBox.offsetLeft;
let y = event.pageY - loginBox.offsetTop;
document.addEventListener('mousemove', loginBoxMove);
// 这里需要把鼠标移动事件函数写在外面,因为移除事件监听器时也会用到!
function loginBoxMove(event) {
loginBox.style.left = (event.pageX - x)+"px";
loginBox.style.top = (event.pageY - y)+"px";
}
document.addEventListener('mouseup', function() {
document.removeEventListener('mousemove', loginBoxMove);
})
});这里为什么不把鼠标松开事件直接绑定给 title 元素?
原因同上,若绑定在 title 上,快速松开鼠标时同样可能因光标脱离元素范围而导致事件无法正确触发或移除,建议读者自行尝试验证。
源代码
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>模态框</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
letter-spacing: 3px;
}
body {
background-color: #ffbf84;
}
/* 立即登录按钮和模态框水平垂直居中 */
.but, .login-box {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* 遮罩层 */
.shade {
display: none;
position: absolute;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .3);
/* 这里一定要把遮罩层移到重叠元素的上层,就可以造成父窗口无法操作的效果 */
z-index: 1;
}
/* 立即登录按钮 */
.but {
cursor: pointer;
display: block;
width: 200px;
height: 60px;
border-radius: 30px;
box-shadow: 0 10px 10px rgba(10, 20, 20, .2);
background-color: #fa8282;
color: #fff;
font-size: 25px;
text-align: center;
line-height: 60px;
}
.but:hover {
transition: background-color 0.5s;
background-color: #f36886;
}
/* 模态框 */
.login-box {
display: none;
width: 350px;
height: 250px;
border: 1px solid rgb(216, 216, 216);
border-radius: 10px;
box-shadow: 5px 5px 10px rgba(10, 20, 20, .2), -5px -5px 10px rgba(10, 20, 20, .2);
background-color: #fff;
/* 和遮罩层同理,模态框一定要在最上层 */
z-index: 1;
}
.login-box .title {
cursor: move;
user-select: none; /* 禁止用户选中文字 */
position: relative;
width: 100%;
height: 70px;
color: #3f3f3f;
font-size: 20px;
font-weight: 700;
text-align: center;
line-height: 70px;
}
.login-box .title .exit {
position: absolute;
top: -10px;
right: 10px;
font-size: 30px;
}
.login-box .title .exit:hover {
cursor: pointer;
text-shadow: 2px 2px 4px rgba(10, 20, 20, .5);
}
.login-box form {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 10px 0;
}
.login-box form .input-box {
width: 60%;
height: 35px;
margin-bottom: 20px;
padding: 0px 10px;
border: 1px solid #3f3f3f;
border-radius: 8px;
color: #3f3f3f;
font-size: 16px;
font-weight: 700;
}
.login-box form .input-box:focus {
outline: none;
}
.login-box form .login-but {
width: 120px;
height: 35px;
border: none;
background-color: #fa8282;
border-radius: 8px;
color: #fff;
font-size: 20px;
font-weight: 700;
}
.login-box form .login-but:hover {
background-color: #f36886;
}
</style>
</head>
<body>
<!-- 遮罩层 -->
<div class="shade"></div>
<!-- 登录按钮 -->
<span class="but">立即登录</span>
<!-- 模态框 -->
<div class="login-box">
<div class="title">
用户登录
<span class="exit">×</span>
</div>
<form action="">
<input type="text" class="input-box" placeholder="用户名">
<input type="password" class="input-box" placeholder="密码">
<input type="submit" class="login-but" value="登录">
</form>
</div>
<script>
let but = document.querySelector('.but');
let shade = document.querySelector('.shade');
let loginBox = document.querySelector('.login-box');
let title = document.querySelector('.title');
let exit = document.querySelector('.exit');
// 立即登录按钮点击事件
but.addEventListener('click', function() {
shade.style.display = "block";
loginBox.style.display = "block";
});
// 关闭模态框事件
exit.addEventListener('click', function() {
shade.style.display = "none";
loginBox.style.display = "none";
});
// 拖动标题区域可移动模态框
title.addEventListener('mousedown', function(event) {
// 计算鼠标在登录框中坐标
let x = event.pageX - loginBox.offsetLeft;
let y = event.pageY - loginBox.offsetTop;
// 给页面添加鼠标移动事件
document.addEventListener('mousemove', loginBoxMove);
function loginBoxMove(event) {
loginBox.style.left = (event.pageX - x)+"px";
loginBox.style.top = (event.pageY - y)+"px";
}
// 鼠标松开后移除页面的鼠标移动事件
document.addEventListener('mouseup', function() {
document.removeEventListener('mousemove', loginBoxMove);
})
});
</script>
</body>
</html>以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。