Skip to content

鼠标事件 ​

鼠标事件主要有以下几个:

  • 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,效果如下

![recording 4](/notes-assets/02.JavaScript/7.DOM事件/assets/recording 4.gif)

读者会发现一些问题,我这里给 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>

![recording 5](/notes-assets/02.JavaScript/7.DOM事件/assets/recording 5.gif)

可以看到,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>

![recording 6](/notes-assets/02.JavaScript/7.DOM事件/assets/recording 6.gif)

如此便完成了一个拖拽效果,这里请读者思考一个问题,理论上来说,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 元素位于视口的位置,如下所示:
![500](/notes-assets/02.JavaScript/7.DOM事件/assets/Pasted image 20240412151927.png)
这个方法的主要用处就是用来计算鼠标在某个元素内的相对位置。

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 的区别:

  1. 键盘上一般会有两个 alt 和两个 ctrl 按键,code 会区分这两个按键,即 code 实际上是看你按下了哪个按键,左右两个按键实际上是不一样的。
  2. 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 元素自身的属性,而非事件对象里的属性。改属性可以修改,修改这个属性可以用于手动设置滚动条高度。
500
如果我们要获取整个网页的滚动高度,可以使用 document.documentElement.scrollTop 或者 document.body.scrollTop 获取。

resize:窗口尺寸发生改变的时候运行的事件,该事件是 window 对象中的事件,这个事件监听的是视口尺寸。

如果我们要获取网页窗口的尺寸和宽高,这里给一张图

尺寸1

这个尺寸不需要刻意记忆,要用到的时候可以直接查

接下来是一个 DOM 元素的尺寸获取方式

500

775

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>

以笔记为镜,可以知得失