鼠标事件
鼠标事件主要有以下几个:
- click:用户单击主鼠标按钮(一般是左键)或者按下在聚焦时按下回车键时触发
- auxclick:用户单击非主鼠标按钮时触发(右键、中键、侧键),该方法只能使用
addEventListener添加,且存在兼容性问题。 - dblclick:用户双击主鼠标按键触发(频率取决于系统配置)
- mousedown:用户按下鼠标任意按键时触发
- mouseup:用户抬起鼠标任意按键时触发
- mousemove:鼠标在元素上移动时触发
- mouseover:鼠标进入元素时触发
- mouseout:鼠标离开元素时触发
- mouseenter:鼠标进入元素时触发,该事件不会冒泡
- mouseleave:鼠标离开元素时触发,该事件不会冒泡
鼠标点击事件,本质上是在该元素内按下抬起后才会触发。
这里着重区别 mouseout,mouseover 和 mouseleave,mouseenter 之间的区别
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}
.container {
width: 300px;
height: 300px;
border: 1px solid;
}
.item {
width: 100px;
height: 100px;
background-color: red;
}
</style>
</head>
<body>
<div class="container">
<div class="item"></div>
</div>
<script>
var container = document.querySelector('.container');
container.onmouseover = function () {
console.log("鼠标进入");
}
container.onmouseout = function () {
console.log("鼠标离开");
}
</script>
</body>
</html>这里先试用 mouseover 和 mouseout,效果如下

读者会发现一些问题,我这里给 container 设置了移入移出事件,但是当我移到 item,即 container 的子元素时,立刻触发了鼠标移出事件,所以我们可以有这样一个结论,mouseover 和 mouseout 并不会考虑子元素,或者说不把子元素看做自己的一部分,当鼠标移入子元素的时候也会看做是移出。
读者可能会有一些疑问,移入子元素后立刻触发了移出事件,这个好理解,那为什么紧接着又触发了移入事件呢?因为当鼠标移入子元素的时候,触发了子元素的鼠标移入事件,而子元素的鼠标移入事件又冒泡到了外面,从而触发了
container的鼠标移出事件。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}
.container {
width: 300px;
height: 300px;
border: 1px solid;
}
.item {
width: 100px;
height: 100px;
background-color: red;
}
</style>
</head>
<body>
<div class="container">
<div class="item"></div>
</div>
<script>
var container = document.querySelector('.container');
container.onmouseenter = function () {
console.log("鼠标进入");
}
container.onmouseleave = function () {
console.log("鼠标离开");
}
</script>
</body>
</html>
可以看到,mouseenter 和 mouseleave 会把子元素也看做是自己,所以当鼠标移入子元素的时候,并不会触发鼠标移出事件。
mouseenter和mouseleave并没有事件冒泡,所以不会向上传递,读者可以通过event.bubbles属性看到一个事件是否冒泡。
所有的鼠标事件的事件对象都是 MouseEvent 构造函数构造的对象,这类事件对象提供了以下常用属性
- altKey:触发事件时,是否按下了键盘的 alt 键
- ctrlKey:触发事件时,是否按下了键盘的 ctrl 键
- shiftKey:触发事件时,是否按下了键盘的 shift 键
- button:触发事件时,鼠标按键类型
- 0:左键
- 1:中键
- 2:右键
- 3:侧键 1
- 4:侧键 2
- page:pageX、pageY,当前鼠标距离页面的横纵坐标
- client: clientX、clientY,鼠标相对于视口的坐标
- offset:offsetX、offsetY,鼠标相对于事件源
padding-box的坐标 - screen: screenX、screenY,鼠标相对于屏幕
- x、y,等同于 clientX、clientY
- movement:movementX、movementY,只在鼠标移动事件中有效,相对于上一次鼠标位置,偏移的距离
以下利用上面的事件和事件对象,我们做一个拖拽效果
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}
.box {
width: 300px;
height: 300px;
background-color: red;
position: absolute;
left: 0;
right: 0;
cursor: move;
}
</style>
</head>
<body>
<div class="box">
</div>
<script>
// 获取页面中class为'box'的元素
var box = document.querySelector('.box');
var offx = 0; // 记录鼠标按下时相对元素左上角的横向偏移
var offy = 0; // 记录鼠标按下时相对元素左上角的纵向偏移
/**
* 处理鼠标按下事件
* @param e 事件对象,代表鼠标按下时的事件信息
*/
function mouseDown(e) {
// 添加鼠标移动和松开事件监听
document.addEventListener("mousemove", mouseMove);
document.addEventListener("mouseup", mouseUp);
// 记录鼠标按下位置相对于元素的偏移
offx = e.offsetX;
offy = e.offsetY;
}
/**
* 处理鼠标移动事件
* @param e 事件对象,代表鼠标移动时的事件信息
*/
function mouseMove(e) {
// 计算并设置元素新的left和top值,使元素跟随鼠标移动
var left = (e.pageX - offx) + "px";
var top = (e.pageY - offy) + "px";
box.style.left = left;
box.style.top = top;
}
/**
* 处理鼠标松开事件
* @param e 事件对象,代表鼠标松开时的事件信息
*/
function mouseUp(e) {
// 移除鼠标移动事件监听,停止元素跟随鼠标移动
document.removeEventListener("mousemove", mouseMove);
}
// 给box元素添加鼠标按下事件监听,开始拖动逻辑
box.addEventListener("mousedown", mouseDown);
</script>
</body>
</html>
如此便完成了一个拖拽效果,这里请读者思考一个问题,理论上来说,mousemove 和 mouseup 事件应该绑定在 box 上,但我这里的 mousemove 事件和 mouseup 事件是绑定在 document 上的,为什么呢?或者说,如果绑定在 box 上,会出现什么问题呢?这个问题请读者自行实验或者思考。
下面是两个练习
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
/* 重置样式 */
* {
margin: 0;
padding: 0;
}
/* 定义盒子样式 */
.box {
float: left;
border: 1px solid black;
}
/* 隐藏元素样式 */
.hidden {
display: none;
}
/* 可移动的盒子样式 */
.box1 {
position: relative;
cursor: move;
}
/* 画布样式 */
.draw {
position: absolute;
background-color: rgba(210, 255, 8, 0.3);
}
</style>
</head>
<body>
<div class="box box1">
<div class="draw hidden">
</div>
</div>
<div class="box box2 hidden"></div>
<script>
// 获取画布和盒子元素
var draw = document.querySelector('.draw');
var box1 = document.querySelector('.box1');
var box2 = document.querySelector('.box2');
// 设置盒子大小
var size = 500;
box1.style.height = size + "px";
box1.style.width = size + "px";
box2.style.height = size + "px";
box2.style.width = size + "px";
// 设置盒子背景图片
box1.style.backgroundImage = "url(https://picsum.photos/" + size + ")";
box2.style.backgroundImage = "url(https://picsum.photos/" + size + ")";
// 设置画布大小和背景图片缩放比例
var rate = 5;
draw.style.width = size / rate + "px";
draw.style.height = size / rate + "px";
box2.style.backgroundSize = size * rate + "px " + size * rate + "px";
// 监听鼠标进入、离开和移动事件
box1.addEventListener("mouseenter", function () {
box2.classList.remove("hidden");
draw.classList.remove("hidden");
});
box1.addEventListener("mouseleave", function () {
box2.classList.add("hidden");
draw.classList.add("hidden");
});
box1.addEventListener("mousemove", function (e) {
// 计算画布位置并限制在盒子内
var left = (e.clientX - box1.getBoundingClientRect().left) - 0.5 * size / rate;
var top = (e.clientY - box1.getBoundingClientRect().top) - 0.5 * size / rate;
if (left < 0)
left = 0;
if (left > size - size / rate)
left = size - size / rate;
if (top < 0)
top = 0;
if (top > size - size / rate)
top = size - size / rate;
draw.style.left = left + "px";
draw.style.top = top + "px";
// 更新第二个盒子的背景位置
box2.style.backgroundPosition = `${-left * rate}px ${-top * rate}px`;
});
</script>
</body>
</html>在这个例子中,着重关注
getBoundingClientRect()方法,该方法会返回一个对象,这个对象保存当前 DOM 元素位于视口的位置,如下所示:

这个方法的主要用处就是用来计算鼠标在某个元素内的相对位置。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
/* 重置样式,确保元素没有额外的外边距和内边距 */
* {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div class="container">
<img src="./img/empty.png" alt="">
<img src="./img/empty.png" alt="">
<img src="./img/empty.png" alt="">
<img src="./img/empty.png" alt="">
<img src="./img/empty.png" alt="">
<span></span>
</div>
<script>
// 获取页面中的关键元素,包括评分容器、所有图片元素和一个用于显示评分的span元素
var span = document.querySelector('span');
var imgs = document.querySelectorAll('img');
var container = document.querySelector('.container');
var currentStar = null; // 当前鼠标悬停的星标
var grade = ["啥也不是", "很差", "一般", "很好", "非常好"]; // 分级描述
// 监听容器的鼠标移动事件,实现星标悬停效果和评分显示
container.addEventListener("mousemove", function (e) {
// 如果当前目标是已选中的星标或者不是图片元素,则不进行处理
if (e.target === currentStar || e.target.nodeName != "IMG")
return;
currentStar = e.target; // 更新当前悬停的星标
currentStar.src = "./img/shining.png"; // 为当前悬停的星标显示亮起效果
// 为之前的所有星标显示亮起效果
for (var i = 0; imgs[i] !== currentStar; i++) {
imgs[i].src = "./img/shining.png";
}
span.innerText = grade[i]; // 更新评分显示
// 为之后的所有星标重置为未选中状态
for (i++; i < imgs.length; i++) {
imgs[i].src = "./img/empty.png";
}
});
</script>
</body>
</html>键盘事件
- keydown:按下键盘上任意键触发,如果按住不放,会重复触发此事件
- keypress:按下键盘上一个字符键时触发(读者可以认为是输入内容时触发)
- keyup:抬起键盘上任意键触发
注:
keypress事件是一个被弃用的事件,应该避免使用,不信你自己看:元素:keypress 事件 - Web API 接口参考 | MDN
对于输入框类的元素,当我们按下一个字符的时候,会输入一个文字,这个输入文字的操作实际上是这个元素
keydown事件的默认行为,如果我们要阻止一个文本框输入文本,只需要阻止keydown事件的默认行为即可,但即便阻止了默认的输入事件,也不影响keydown和keypress事件的触发。我们可以利用这个特点来让文本框只能输入某些字符。
键盘事件的事件对象为 KeyboardEvent 构造函数构造出来的对象,这个对象曾经有非常多的属性,但是现在已经不多了。
- ctrlKey:是否按住 ctrl
- shiftKey:是否按住了 shift
- altKey:是否按住了 alt 键
- code:得到按键字符串。
- key:得到按键字符串。
这里注意 key 和 code 的区别:
- 键盘上一般会有两个 alt 和两个 ctrl 按键,code 会区分这两个按键,即 code 实际上是看你按下了哪个按键,左右两个按键实际上是不一样的。
- key 更多的是注重你输入了什么,比如左右的 ctrl,在 key 眼里,虽然这是两个按键,但是输入的都是 ctrl 这个东西,所以得到的是一样的。同样,一般来说,按下 shift 再输入 2,得到的应该是一个
@字符,如果是 code 属性,那么不管你输入的是什么,你按下的始终是 2 这个按钮,所以你按不按 shift 按键得到的就是一样的,但如果是 key,它更注重你输入什么,所以当你按下shift+2的时候,key会得到@,不按shift的时候得到的就是2。
对于键盘事件,尤其注意,一般来说,我们都要给一个可聚焦的元素注册键盘事件,比如 input,textarea 这种,如果是一个不可聚焦的元素,或者说简单点就是不可选中的元素,比如说一个 div,你给一个单独的 div 注册键盘事件是毫无效果的,除非它里面有一个可聚焦的元素,这样可以冒泡冒上来。所以在大多数场景下,都推荐给一个很大的元素注册键盘事件,比如 document,比如 window,只要给这俩注册了键盘事件,那么在全局任何地方按下键盘都是有效的。
下面是一些练习:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
/* 重置样式,确保元素没有额外的外边距和内边距 */
* {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<input type="text" class="number">
<script>
/**
* 监听输入框的keydown事件,阻止非数字键的输入。
* @param {Object} e 事件对象,代表键盘输入事件。
*/
var input = document.querySelector('.number'); // 获取类名为number的输入元素
input.addEventListener("keydown", function (e) {
// 如果按下的键不是数字,则阻止默认行为
if (Number.isNaN(Number(e.key))) {
console.log(Number(e.key));
e.preventDefault();
}
});
</script>
</body>
</html>html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
/* 重置样式,确保元素没有额外的外边距和内边距 */
* {
margin: 0;
padding: 0;
}
body {
background-color: black;
}
img {
position: absolute;
}
</style>
</head>
<body>
<img src="./imgs/tankR.gif" class="tank" alt="">
<script>
// 获取页面中class为'tank'的元素
var tank = document.querySelector('.tank');
// 初始化坦克的初始左偏移量
currentLeft = 100;
// 初始化坦克的初始顶偏移量
currentTop = 100;
// 设置坦克的初始位置
tank.style.left = currentLeft + 'px';
tank.style.top = currentTop + 'px';
// 监听键盘按键事件,实现坦克的移动
document.addEventListener("keydown", function (e) {
// 定义坦克每次移动的距离
var step = 5;
// 如果按下Shift键,增加移动距离
if (e.shiftKey) {
step += 5;
}
// 根据不同的按键方向,移动坦克并改变坦克图片
switch (e.key) {
case "ArrowUp":
// 如果向上移动后位置为负数,则不移动
if (currentTop - step < 0) {
return;
} else {
currentTop -= step;
tank.style.top = currentTop + 'px';
tank.src = "./imgs/tankU.gif";
}
break;
case "ArrowLeft":
// 如果向左移动后位置为负数,则不移动
if (currentLeft - step < 0) {
return;
} else {
currentLeft -= step;
tank.style.left = currentLeft + 'px';
tank.src = "./imgs/tankL.gif";
}
break;
case "ArrowRight":
// 向右移动坦克并改变坦克图片
currentLeft += step;
tank.style.left = currentLeft + 'px';
tank.src = "./imgs/tankR.gif";
break;
case "ArrowDown":
// 向下移动坦克并改变坦克图片
currentTop += step;
tank.style.top = currentTop + 'px';
tank.src = "./imgs/tankD.gif";
break;
}
});
</script>
</body>
</html>表单事件
- focus:元素聚焦的时候触发(能与用户发生交互的元素,都可以聚焦),该事件不会冒泡
- blur:元素失去焦点时触发,该事件不会冒泡。
- submit:提交表单事件,该事件一般添加在 form 表单元素上,但是也可以添加在父元素上,冒泡出去也可以监听到,但是一般不会这么干。
- change:文本改变事件,改变后再失去焦点才会触发
- input: 文本改变事件,只要输入了就会触发
submit 事件是 form 表单的提交事件,比如 form 表单内按下回车,或者点击提交按钮都会触发,触发提交事件后,默认会跳转到提交的页面,或者刷新页面,一般来说我们更喜欢用 ajax 去进行请求响应,所以大多数情况下我们会阻止这个默认事件。
页面状态事件
页面状态事件一般是 window 对象上的事件,有一些事件在 DOM 元素上也可以使用。
load 事件:如果绑定在 window 对象上,那么页面中所有资源全部加载完成后触发该事件。
默认情况下,元素,包括一些资源,都是同步加载的,也就是说,如果我写了一个需要外部资源的元素,例如 link,script,都是同步渲染的,当浏览器渲染到这里的时候,会阻塞等待,等资源加载完成之后才会继续加载。当然,我们也可以在这些节点元素上加上 async 属性,让其异步加载。
注意,多媒体资源是异步加载的,所以在加载的过程中不会阻塞,在图片加载完成之前,如果我们去获取其宽高,是会获取失败的。
对于图片元素,可以直接通过imgElement.width,imgElement.height获取其宽高,如果 CSS 没有设置,也可以获取到图片的原始尺寸。
这里就可以解释,为什么 script 元素需要写在所有元素下面,正是由于同步加载,如果写在 DOM 元素上方,那么当运行 JS 代码的时候,DOM 元素都没有生成,所以你根本获取不到 DOM 元素,只有写在所有元素下方,让 DOM 元素都生成完毕了,此时才能获取到 DOM 元素。同样,CSS 应该放在上面,要先加载样式,再加载元素,如果 CSS 写在了后面,那么浏览器会先把没有样式的 DOM 元素加载出来,然后读到 CSS 之后重新添加样式,这样就有可能出现闪烁问题。
而 load 事件,是所有资源都加载完毕之后才会触发,包括异步资源,所以执行时间是很靠后的,由于 load 事件触发的时候,所有的资源全部加载完毕了,所以在 load 事件触发的时候,我们就可以获取到图片的宽高信息了。但是我们一般不会给 window 对象添加 load 事件,因为在 window 对象上添加 load 事件之后,是要等所有资源全部加载完成之后,才会执行,这个太靠后了。如果我们要获取图片的宽高,我们只需要等这个图片资源加载完成之后就可以获取了,所以像这个 load 事件,通常添加在图片元素上,此时在这个图片资源加载完成后就可以触发这个事件,也就可以在这个事件内部获取图片的宽高。
从这里读者也应该知道,load 事件其实并不只可以注册在 window 上,也可以注册在其他元素上。
由于 window 上的 load 事件太过于晚了,所以我们也可以用下面两个事件
DOMContentLoaded:该事件是 document 上的事件,是在 DOM 树构造完成之后执行。该事件必须使用addEventListener方式注册,由于该事件在 jQuery 里也是 ready 事件,所以有时候我们也称为 ready 事件。如果我希望我的 script 标签无论放在页面的什么位置都可以正常执行,那么就得写在这个事件内部。readystatechange:该事件也是 document 上的事件,页面加载状态变化的时候触发。页面在渲染的时候有三个状态,分别是 loading, interactive, complete,分别表示正在加载、DOM 树构建完成、页面加载完成。我们可以用document.readyState来获取到当前状态。实际上,这个事件是可以替代DOMContentLoaded和load事件的,但是我们一般不会用这个。
这里考虑一个问题,我需要得写一个函数,用于得到图片的尺寸。
js
function getImageSize(img,callback){
if(img.width == 0 && img.height == 0){
img.onload = function(){
callback({
width:img.width,
height:img.height,
});
}
}
}
getImageSize(img,function(size){
console.log(size);//这里可以得到图片尺寸
})这里请读者思考一下,为什么要使用这种回调函数的模式。
beforeunload 事件:这个事件是 window 对象上的事件,在窗口关闭前触发,在以前,该事件可以通过返回字符串的方式用于阻止窗口关闭,但是现在主流浏览器已经不行了。
unload 事件:这个事件也是关闭窗口时运行,在 beforeunload 事件之后触发,该事件无法用于阻止窗口关闭
scroll 事件:窗口发生滚动的时候触发该事件,该事件不仅可以写在 window 上,也可以写在元素上,因为元素的 overflow 属性如果设置为 auto 或者 scroll,那么也会有滚动条,此时该元素滚动的时候也会触发该元素上的 scroll 事件。
我们可以使用
dom.scrollTop和dom.scrollLeft来获取dom元素的滚动距离,尤其注意,这个的 dom 元素自身的属性,而非事件对象里的属性。改属性可以修改,修改这个属性可以用于手动设置滚动条高度。
如果我们要获取整个网页的滚动高度,可以使用document.documentElement.scrollTop或者document.body.scrollTop获取。
resize:窗口尺寸发生改变的时候运行的事件,该事件是 window 对象中的事件,这个事件监听的是视口尺寸。
如果我们要获取网页窗口的尺寸和宽高,这里给一张图

这个尺寸不需要刻意记忆,要用到的时候可以直接查
接下来是一个 DOM 元素的尺寸获取方式


contextmenu:上下文菜单事件,一般浏览器点击右键出现的就是 contextmenu,我们一般称为右键菜单。这个上下文菜单可以给元素注册,也可以给 window 注册。我们可以阻止默认行为,阻止之后右键就不会再出现右键菜单。
paste:粘贴事件,粘贴的时候触发,一些网站,特别是编程网站,为了防止你粘贴别人的代码,就会用这个阻止默认事件。
copy:复制事件,复制的时候触发,一些网站在复制的时候会弹出窗口让你充钱,就是在这个事件里阻止了默认行为。
cut:剪切事件
以上几个事件和复制粘贴有关,他们的事件对象都有一个
clipboardData对象,如果不在事件对象上,那就在 window 对象上。我们可以使用这个对象上的getData方法,传入一个 MIME数据格式,就可以按照指定格式得到剪切板的内容。
下面是一些练习:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
/* 重置样式,确保元素没有额外的外边距和内边距 */
* {
margin: 0;
padding: 0;
list-style: none;
}
.container {
width: 500px;
height: 500px;
background-color: aqua;
position: relative;
}
.myMenu {
background-color: aliceblue;
position: absolute;
width: 100px;
padding: 5px;
}
.myMenu li {
padding: 5px;
user-select: none;
}
.myMenu li:hover {
background-color: antiquewhite;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div class="container">
<ul class="myMenu hidden">
<li>菜单1</li>
<li>菜单2</li>
<li>菜单3</li>
</ul>
</div>
<script>
// 获取页面中class为container的元素和myMenu元素
var container = document.querySelector('.container');
var myMenu = document.querySelector('.myMenu');
// 为container元素绑定右键菜单事件
container.addEventListener("contextmenu", function (e) {
e.preventDefault(); // 阻止默认右键菜单出现
if (e.target !== container) {
// 如果右键点击的不是container元素,则调用myMenuClick函数
myMenuClick(e);
return;
}
// 如果右键点击的是container元素,显示自定义菜单
myMenu.classList.remove('hidden');
// 设置自定义菜单的位置
if (e.target == container) {
myMenu.style.left = e.offsetX + 'px';
myMenu.style.top = e.offsetY + 'px';
}
});
// 为container元素绑定点击事件,用于隐藏自定义菜单
container.addEventListener("click", function (e) {
if (e.target == container) {
myMenu.classList.add("hidden");
}
});
/**
* 处理myMenu元素点击事件的函数
* @param {Object} e 事件对象
*/
function myMenuClick(e) {
console.log(e.target.innerText); // 打印点击的菜单项文本
myMenu.classList.add("hidden"); // 隐藏菜单
}
// 为myMenu元素绑定点击事件
myMenu.addEventListener("click", myMenuClick);
</script>
</body>
</html>html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
/* 重置样式,确保元素没有额外的外边距和内边距 */
* {
margin: 0;
padding: 0;
list-style: none;
}
html,
body {
height: 100%;
}
.vow {
position: fixed;
width: 200px;
height: 50px;
padding: 0 20px;
font-size: 20px;
left: 0;
right: 0;
bottom: 50px;
margin-left: auto;
margin-right: auto;
}
.container {
height: 100%;
}
.container .box {
box-sizing: border-box;
position: absolute;
padding: 10px;
border-radius: 10px;
user-select: none;
cursor: move;
}
.container .close {
position: absolute;
right: 10px;
top: 10px;
user-select: none;
cursor: pointer;
}
.container .content {
margin-top: 15px;
}
</style>
</head>
<body>
<div class="container">
</div>
<input type="text" placeholder="请输入你的愿望" class="vow">
<script>
var boxSize = 150; // 盒子的大小
var container = document.querySelector('.container'); // 页面中的容器元素
var activeBox = null; // 当前被操作的盒子
var input = document.querySelector('.vow'); // 输入框,用于输入盒子内容
var disx; // 鼠标按下时的水平偏移量
var disy; // 鼠标按下时的垂直偏移量
var clientWidth = document.documentElement.clientWidth; // 页面可视宽度
var clientHeight = document.documentElement.clientHeight; // 页面可视高度
var maxIndex = 0; // 盒子的z-index最大值
/**
* 创建并添加一个愿望盒子到页面
* @param {string} str 盒子的内容
* @param {number} left 盒子的初始左偏移(可选)
* @param {number} top 盒子的初始上偏移(可选)
* @returns {HTMLElement} 创建的盒子元素
*/
function createBox(str, left, top) {
// 创建盒子元素并设置样式
var box = document.createElement('div');
box.classList.add("box");
var close = document.createElement('div'); // 创建关闭按钮
close.classList.add("close");
close.innerHTML = 'X';
var content = document.createElement('div'); // 创建内容区域
content.classList.add("content");
content.innerHTML = str;
box.append(close, content); // 将关闭按钮和内容添加到盒子
box.style.left = (left || 0) + 'px';
box.style.top = (top || 0) + 'px';
box.style.height = boxSize + 'px';
box.style.width = boxSize + 'px';
box.style.backgroundColor = `rgb(${Math.random() * 100 + 100},${Math.random() * 100 + 100},${Math.random() * 100 + 100})`; // 随机背景色
// 关闭按钮点击事件,移除盒子
close.addEventListener("click", function () {
this.parentElement.remove();
});
// 盒子拖动事件处理
function mouseMove(e) {
// 允许盒子移动,并防止移出边界
var left = e.clientX - disx;
var top = e.clientY - disy;
if (left + boxSize >= clientWidth) {
left = clientWidth - boxSize;
}
if (top + boxSize >= clientHeight) {
top = clientHeight - boxSize;
}
if (left <= 0) {
left = 0;
}
if (top <= 0) {
top = 0;
}
activeBox.style.left = left + 'px';
activeBox.style.top = top + 'px';
}
// 盒子按下事件,设置拖动和z-index
box.addEventListener("mousedown", function (e) {
this.style.zIndex = ++maxIndex;
activeBox = this;
disx = e.clientX - Number.parseFloat(this.style.left);
disy = e.clientY - Number.parseFloat(this.style.top);
document.addEventListener("mousemove", mouseMove);
document.addEventListener("mouseup", function (e) {
document.removeEventListener("mousemove", mouseMove);
if (activeBox)
activeBox.classList.remove("active");
});
});
box.style.zIndex = ++maxIndex; // 设置盒子的z-index以确保其在顶层
return box;
}
// 窗口大小改变时调整盒子位置以防止移出视窗
window.addEventListener("resize", function () {
clientWidth = document.documentElement.clientWidth;
clientHeight = document.documentElement.clientHeight;
var allBox = container.children;
Array.from(container.children).forEach(function (item) {
if (Number.parseFloat(item.style.left) + boxSize >= clientWidth)
item.style.left = clientWidth - boxSize + 'px';
if (Number.parseFloat(item.style.top) + boxSize >= clientHeight)
item.style.top = clientHeight - boxSize + 'px';
});
});
// 输入框按下Enter键时创建并添加一个新盒子
input.addEventListener("keydown", function (e) {
if (e.key !== "Enter")
return;
var randomLeft = Math.floor(Math.random() * (clientWidth - boxSize));
var randomTop = Math.floor(Math.random() * (clientHeight - boxSize));
console.log(createBox(this.value, randomLeft, randomTop));
var newBox = createBox(this.value, randomLeft, randomTop);
container.append(newBox);
this.value = "";
});
</script>
</body>
</html>