用css和js制作类似于win10的一级和二级菜单

        记录第一次玩js。这个想法我看了很多文章,但都不太理想。包括我自己写的也不太理想,虽然说再花点时间能把问题给解决了,但是写完以后我自己都忍不住笑自己。进步的空间实在是太大了,发出来的目的是为了让未来的我警醒自己的水平是真的拉!!!真的是小看这小小网页了。虽然比qt拓展性强,但是覆盖的知识面挺广的,看来只能用时间慢慢地去磨了。

还没解决的问题:

1.当菜单出现在最右侧的时候子菜单不能从左边出现

2.不能让菜单自适应文本长度(当文本超出框框的时候自动换行,不会根据文本长短来自动调整菜单的长度,也不会给那个打勾添加到文本的末尾)

3.勾选状态只能单个出现在某个子菜单中,不能同时出现在多个子菜单中(已解决)

4.两次点击同一个子菜单项的时候,状态没被取消

5.这其实还有一个问题,就是我右击页面就可以正常创建一个菜单,但是当我右击菜单的时候,菜单却出现在左上角,这个需要注意,这个实在是不知道该怎么改过来

参考过的代码网址:

每天一个小技巧:实现自定义右键菜单(Context Menu) - 掘金

JS实现页面右键菜单--优化版(二) - 掘金


一级菜单的完整代码如下:(这个是抄别人的!)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    * {
      padding: 0;
      margin: 0;
      background-color: #f0f2f7;
    }

    .custom-context-menu {
      position: fixed;
      border: 1px solid #ccc;
      list-style: none;
      padding: 4px 0;
      border-radius: 4px;
      box-shadow: 0px 2px 6px 2px #ddd;
      background-color: #fff;
      color: #000;
      width: 160px;
      animation: fade-in 0.2s ease-in-out;
      z-index: 1;
    }

    .hidden {
      display: none;
    }

    li {
      /*display: inline-block;*/
      padding: 8px 6px;
      border-bottom: 1px solid #f0f2f5;
      user-select: none;
      transition: all 0.1s;
      z-index: 2;
    }

    li:hover {
      cursor: pointer;
      background-color: #0170fe;
      color: #fff;
    }
  </style>
</head>
<body>
<script>
  const ContextMenu = function (options) {
    let instance;

    function createMenu() {
      const ul = document.createElement("ul");
      ul.classList.add("custom-context-menu");
      const { menus } = options;
      if (menus && menus.length > 0) {
        for (let menu of menus) {
          const li = document.createElement("li");
          li.textContent = menu.name;
          li.onclick = menu.onClick;
          ul.appendChild(li);
        }
      }
      const body = document.querySelector("body");
      body.appendChild(ul);
      return ul;
    }

    return {
      getInstance: function () {
        if (!instance) {
          instance = createMenu();
        }
        return instance;
      },
    };
  };

  const contextMenu = ContextMenu({
    menus: [
      {
        name: "custom menu 1",
        onClick: (e)=> {
          console.log("menu1 clicked");
        },
      },
      {
        name: "custom menu 2",
        onClick: (e)=> {
          console.log("menu2 clicked");
        },
      },
      {
        name: "custom menu 3",
        onClick: (e)=> {
          console.log("menu3 clicked");
        },
        children:[{
          name: "222",
        }]
      },
    ],
  });

  function showMenu(e) {
    e.preventDefault();
    const menus = contextMenu.getInstance();
    menus.style.top = `${e.clientY}px`;
    menus.style.left = `${e.clientX}px`;
    menus.classList.remove("hidden");
  }

  function hideMenu(e) {
    const menus = contextMenu.getInstance();
    menus.classList.add("hidden");
  }

  document.addEventListener("contextmenu", showMenu);
  document.addEventListener("click", hideMenu);

</script>
</body>
</html>

二级菜单完整代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test</title>
    <style>
        * {
            padding: 0;
            margin: 0;
            background-color: #f0f2f7;
        }

        .custom-context-menu {
            position: fixed;
            border: 1px solid #ccc;
            list-style: none;
            padding: 4px 0;
            border-radius: 4px;
            box-shadow: 0px 2px 6px 2px #ddd;
            background-color: #fff;
            color: #000;
            width: 160px;
            animation: fade-in 0.2s ease-in-out;
            z-index: 1;
        }

        .hidden {
            display: none;
        }

        li {
            /*display: inline-block;*/
            padding: 8px 6px;
            border-bottom: 1px solid #f0f2f5;
            user-select: none;
            transition: all 0.1s;
            z-index: 2;
        }

        :last-child {
            border-bottom: none;
        }

        li:hover {
            cursor: pointer;
            background-color: #0170fe;
            color: #fff;
        }

        li.selected::after {
            content: '\2713';
            display: inline-block;
            margin-left: 5px;
        }

        .submenu {
            position: absolute;
            left: 100%;
            top: -1px;
            visibility: hidden;
        }

        li:hover > .submenu {
            visibility: visible;
        }

        @keyframes fade-in {
            from {
                opacity: 0;
                transform: translateY(-10px);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }
    </style>
</head>
<body>
<script>
    function createSubMenu(menus) {
        const ul = document.createElement("ul");
        ul.classList.add("custom-context-menu");
        ul.classList.add("submenu");
        if (menus && menus.length > 0) {
            for (let menu of menus) {
                const li = document.createElement("li");
                li.textContent = menu.name;
                li.onclick = menu.onClick;
                ul.appendChild(li);
            }
        }
        return ul;
    }

    const ContextMenu = function (options) {
        let instance;

        function createMenu() {
            const ul = document.createElement("ul");
            ul.classList.add("custom-context-menu");
            const {menus} = options;
            if (menus && menus.length > 0) {
                for (let menu of menus) {
                    const li = document.createElement("li");
                    li.textContent = menu.name;
                    li.onclick = menu.onClick;

                    if (menu.children) {
                        const submenu = createSubMenu(menu.children);
                        li.appendChild(submenu);
                    }

                    ul.appendChild(li);
                }
            }
            const body = document.querySelector("body");
            body.appendChild(ul);
            return ul;
        }

        return {
            getInstance: function () {
                if (!instance) {
                    instance = createMenu();
                }
                return instance;
            },
        };
    };

    const contextMenu = ContextMenu({
        menus: [
            {
                name: "custom menu 1",
                onClick: (e) => {
                    console.log("menu1 clicked");
                },
                children: [
                    {
                        name: "submenu item 1-1",
                        onClick: (e) => {
                            console.log("submenu item 1 clicked");
                            const selected = document.querySelector('.custom-context-menu li.selected');
                            if (selected) {
                                selected.classList.remove('selected');
                            }
                            e.target.classList.add('selected');
                        },
                    },
                    {
                        name: "submenu item 1-2",
                        onClick: (e) => {
                            console.log("submenu item 2 clicked");
                            const selected = document.querySelector('.custom-context-menu li.selected');
                            if (selected) {
                                selected.classList.remove('selected');
                            }
                            e.target.classList.add('selected');
                        },
                    },
                    {
                        name: "submenu item 1-3",
                        onClick: (e) => {
                            console.log("submenu item 3 clicked");
                            const selected = document.querySelector('.custom-context-menu li.selected');
                            if (selected) {
                                selected.classList.remove('selected');
                            }
                            e.target.classList.add('selected');
                        },
                    },
                ]
            },
            {
                name: "custom menu 2",
                onClick: (e) => {
                    console.log("menu2 clicked");
                },
                children: [
                    {
                        name: "submenu item 2-1",
                        onClick: (e) => {
                            console.log("submenu item 1 clicked");
                            const selected = document.querySelector('.custom-context-menu li.selected');
                            if (selected) {
                                selected.classList.remove('selected');
                            }
                            e.target.classList.add('selected');
                        },
                    },
                    {
                        name: "submenu item 2-2",
                        onClick: (e) => {
                            console.log("submenu item 2 clicked");
                            const selected = document.querySelector('.custom-context-menu li.selected');
                            if (selected) {
                                selected.classList.remove('selected');
                            }
                            e.target.classList.add('selected');
                        },
                    },
                    {
                        name: "submenu item 2-3",
                        onClick: (e) => {
                            console.log("submenu item 3 clicked");
                            const selected = document.querySelector('.custom-context-menu li.selected');
                            if (selected) {
                                selected.classList.remove('selected');
                            }
                            e.target.classList.add('selected');
                        },
                    },
                ]
            },
            {
                name: "custom menu 3",
                onClick: (e) => {
                    console.log("menu3 clicked");
                },
                children: [
                    {
                        name: "submenu item 3-1",
                        onClick: (e) => {
                            console.log("submenu item 1 clicked");
                            const selected = document.querySelector('.custom-context-menu li.selected');
                            if (selected) {
                                selected.classList.remove('selected');
                            }
                            e.target.classList.add('selected');
                        },
                    },
                    {
                        name: "submenu item 3-2",
                        onClick: (e) => {
                            console.log("submenu item 2 clicked");
                            const selected = document.querySelector('.custom-context-menu li.selected');
                            if (selected) {
                                selected.classList.remove('selected');
                            }
                            e.target.classList.add('selected');
                        },
                    },
                    {
                        name: "submenu item 3-3",
                        onClick: (e) => {
                            console.log("submenu item 3 clicked");
                            const selected = document.querySelector('.custom-context-menu li.selected');
                            if (selected) {
                                selected.classList.remove('selected');
                            }
                            e.target.classList.add('selected');
                        },
                    },
                    {
                        name: "submenu item 3-4",
                        onClick: (e) => {
                            console.log("submenu item 4 clicked");
                            const selected = document.querySelector('.custom-context-menu li.selected');
                            if (selected) {
                                selected.classList.remove('selected');
                            }
                            e.target.classList.add('selected');
                        },
                    },
                ]
            },
        ],
    });

    function showMenu(e) {
        e.preventDefault();
        const menus = contextMenu.getInstance();
        const submenu = createSubMenu(menus.children);
        let x = e.offsetX;
        let y = e.offsetY;
        let winWidth = window.innerWidth;
        let winHeight = window.innerHeight;
        let menuWidth = menus.offsetWidth;
        let menuHeight = menus.offsetHeight;
        x = winWidth - menuWidth >= x ? x : winWidth - menuWidth;
        y = winHeight - menuHeight >= y ? y : winHeight - menuHeight;

        menus.style.top = y + 'px';
        menus.style.left = x + 'px';

        if (x > (winWidth - menuWidth - subMenuWidth)) {
            if (subMenuWidth > x) {
                submenu.style.left = '-200px';
            } else {
                submenu.style.left = '';
                submenu.style.right = '-200px';
            }
        } else {
            if (menuWidth + subMenuWidth > winWidth - x) {
                submenu.style.left = '';
                submenu.style.right = '-200px';
            } else {
                submenu.style.left = subMenuWidth + 'px';
                submenu.style.right = '';
            }
        }
        menus.classList.remove("hidden");
    }

    function hideMenu(e) {
        const menus = contextMenu.getInstance();
        menus.classList.add("hidden");
    }

    function Reload() {
        document.addEventListener("contextmenu", showMenu);
        document.addEventListener("click", hideMenu);
    }

    Reload()
</script>
</body>
</html>

下面对代码的一个分析:

1.首先是定义二级菜单的对象数组:

采用menus>children的树状结构,每个对象(menu和children)都内含一个属性(name)和一个方法(onClick):

const contextMenu = ContextMenu({
        menus: [
            {
                name: "custom menu 1",
                onClick: (e) => {
                    console.log("menu1 clicked");
                },
                children: [
                    {
                        name: "submenu item 1-1",
                        onClick: (e) => {
                            console.log("submenu item 1 clicked");
                        },
                    },
                    {
                        name: "submenu item 1-2",
                        onClick: (e) => {
                            console.log("submenu item 2 clicked");
                        },
                    },
                    {
                        name: "submenu item 1-3",
                        onClick: (e) => {
                            console.log("submenu item 3 clicked");
                        },
                    },
                ]
            },
            {
                name: "custom menu 2",
                onClick: (e) => {
                    console.log("menu2 clicked");
                },
                children: [
                    {
                        name: "submenu item 2-1",
                        onClick: (e) => {
                            console.log("submenu item 1 clicked");
                        },
                    },
                    {
                        name: "submenu item 2-2",
                        onClick: (e) => {
                            console.log("submenu item 2 clicked");
                        },
                    },
                    {
                        name: "submenu item 2-3",
                        onClick: (e) => {
                            console.log("submenu item 3 clicked");
                        },
                    },
                ]
            },
            {
                name: "custom menu 3",
                onClick: (e) => {
                    console.log("menu3 clicked");
                },
                children: [
                    {
                        name: "submenu item 3-1",
                        onClick: (e) => {
                            console.log("submenu item 1 clicked");
                        },
                    },
                    {
                        name: "submenu item 3-2",
                        onClick: (e) => {
                            console.log("submenu item 2 clicked");
                        },
                    },
                    {
                        name: "submenu item 3-3",
                        onClick: (e) => {
                            console.log("submenu item 3 clicked");
                        },
                    },
                    {
                        name: "submenu item 3-4",
                        onClick: (e) => {
                            console.log("submenu item 4 clicked");
                        },
                    },
                ]
            },
        ],
    });

2.其次,令一级菜单以单例的形式在ContextMenu()函数中被创建:

    const ContextMenu = function (options) {
        let instance;

        function createMenu() {
            const ul = document.createElement("ul");
            ul.classList.add("custom-context-menu");
            const {menus} = options;
            if (menus && menus.length > 0) {
                for (let menu of menus) {
                    const li = document.createElement("li");
                    li.textContent = menu.name;
                    li.onclick = menu.onClick;

                    if (menu.children) {
                        const submenu = createSubMenu(menu.children);
                        li.appendChild(submenu);
                    }

                    ul.appendChild(li);
                }
            }
            const body = document.querySelector("body");
            body.appendChild(ul);
            return ul;
        }

        return {
            getInstance: function () {
                if (!instance) {
                    instance = createMenu();
                }
                return instance;
            },
        };
    };

二级菜单在createSubMenu()函数中被创建:

    function createSubMenu(menus) {
        const ul = document.createElement("ul");
        ul.classList.add("custom-context-menu");
        ul.classList.add("submenu");
        if (menus && menus.length > 0) {
            for (let menu of menus) {
                const li = document.createElement("li");
                li.textContent = menu.name;
                li.onclick = menu.onClick;
                ul.appendChild(li);
            }
        }
        return ul;
    }

3.接着,用showMenu()展示一级菜单和二级菜单,和hideMenu()来隐藏:

//只有一级菜单的时候的代码
function showMenu(e) {
    e.preventDefault();
    const menus = contextMenu.getInstance();
    menus.style.top = `${e.clientY}px`;
    menus.style.left = `${e.clientX}px`;
    menus.classList.remove("hidden");
}

//只有二级菜单的时候代码
function showMenu(e) {
        e.preventDefault();
        const menus = contextMenu.getInstance();
        const submenu = createSubMenu(menus.children);
        let x = e.offsetX;
        let y = e.offsetY;
        let winWidth = window.innerWidth;
        let winHeight = window.innerHeight;
        let menuWidth = menus.offsetWidth;
        let menuHeight = menus.offsetHeight;
        x = winWidth - menuWidth >= x ? x : winWidth - menuWidth;
        y = winHeight - menuHeight >= y ? y : winHeight - menuHeight;

        menus.style.top = y + 'px';
        menus.style.left = x + 'px';
        if (x > (winWidth - menuWidth - submenu.offsetWidth)) {
            submenu.style.left = '-800px';
        } else {
            submenu.style.left = '';
            submenu.style.right = '-200px';
        }
        menus.classList.remove("hidden");
    }

4.最后,添加上监听器:

    function Reload() {
        document.addEventListener("contextmenu", showMenu);
        document.addEventListener("click", hideMenu);
    }

    Reload()

5.优化:就是在每个子菜单项后面添加一个打勾的状态:

  const selected = document.querySelector('.custom-context-menu li.selected');
  if (selected) {
     selected.classList.remove('selected');
  }
  e.target.classList.add('selected');

这个是大体的思路


下面来描述一下具体的过程:

       在知道建立DOM监听来重写contextmenu的情况下,我要写一个showMenu()函数以此达到目的。首先把自带的实现给取消掉,然后先利用单例模式新建一个一级菜单。这个一级菜单由多个菜单项组成,所以需要创建的元素是ul和li。ul和li的搭配是html固定的,ul作为一个专门存放li的大盒子,可以动态的增删li元素,我们利用li作为菜单项,通过  ul.appendChild(li)  动态地将菜单项li添加进ul。然后将ul添加到body中。这样子就完成一级菜单的创建。

       然后就是一级菜单随着鼠标右击位置而显示。这个就是利用插值${}这个东西对菜单的位置进行更新。然后记得remove("hidden"),要不然这个hidden类一直存在下次就不能打开了。

       接着为什么要设计用单例模式来创建菜单,因为那个菜单在win10中是每右击一个图标而产生的,所以在这个菜单显示出来的时候避免同时出现多个。

        回到createMenu()方法,这个方法主要涉及的是html元素和js的交互,还有怎样将创建出来的js对象数组传进方法中进行操作。首先里面主要含有三大html元素,body,ul,li,li会有n个,但是ul和body只有一个,所以li要用for循环加入到ul中,由于我们是纯js,所以要用自带的createElement()函数创建html元素对象,再用appendChild()函数实现包含关系,做完这些就可以直接往菜单列表中添加菜单项而不用一个一个修改菜单项了。这里还有一个很重要的函数就是classList(),这个函数也是js自带的,就是给css提供类选择器,实现由js完成的样式的修改,例如我整个菜单的样式,或者是菜单的隐藏和显示。里面还有参数的拷贝需要注意一下,首先是对象的拷贝,从options拷贝到menus需要{}进行一个声明,menus对象也变成了一个数组,需要对其进行一个遍历,还有就是将对象的属性和方法进行一个拷贝。

        现在来到了二级菜单。二级菜单除了要在菜单对象数组里嵌套新的数组以外,还要仿一级菜单的形式新建ul>li这个关系,逻辑关系和一级菜单差不多的其实(但是当时傻傻的想不出来,怎么我加了以后就没有效果呢,就是没有想到仿照新建ul>li这样的关系,以为只需要在嵌套一层子数组就够了),然后还是appendChild()添加二级菜单到li里面。

        现在我想对这个菜单进行一个创新,就是在已点击的子菜单项中增加一个打勾的状态,就想利用这个onclick()作为一个信号,传递给状态改变的函数,由css添加上一个打勾。但是我的打勾不能同时勾到多个项。(已解决=>之间就是像在每个onclick事件中添加一个打勾对象而已,但在写这篇的东西的时候,突发奇想地将这个打勾对象定义到全局里面,然后就可以实现对多个项进行打勾了)。

       那么,现在出现的问题是如果在边界创建的话,菜单就会被隐藏掉一部分,菜单只能在光标的右方创建,这明显和我设想的差距挺大的,所以就要对菜单的位置进行一个判断(具体参考这篇文章JS实现页面右键菜单--优化版(二) - 掘金)。解决完这个问题之后,又出现了一级菜单能够完整地被显示出来,但是二级菜单只能在右边生成啊,该怎样解决这个问题呢?我原本是照搬这篇文章的做法的,但是到我这里就出了问题,我在边界右击菜单的时候二级菜单被边界隐藏掉了,没有像文章中一样显示出来。现在考虑了几种写法都解决不了,算了把,不能在这上面浪费更多时间了。就这样,有空再更新。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值