原生js手动实现一个多级菜单效果(高度可过渡变化)

news2025/6/13 17:38:19

文章目录

    • 学习链接
    • 效果图
    • 代码
      • 要点

学习链接

vue实现折叠展开收缩动画 - 自己的链接

elment-ui/plus不定高度容器收缩折叠动画组件 - 自己的链接

Vue transition 折叠类动画自动获取隐藏层高度以及手风琴效果实现

vue transition动画钩子- vue官网

vue transition 过渡动画

基于vue渐变展开收起盒子动画(盒子高度不定)

效果图

在这里插入图片描述

代码

要点

  • 需要注意这个dom结构,
  • 过渡动画一定要有开始和结束值才能产生动画,并且在js里面修改的时候,不能连着修改,要把第二次修改放到setTimeout里面
  • 为了让菜单能够不是一次性过渡(让它可以一直产生过渡动画),需要在动画结束后,清理掉设置的高度,这个设置的高度只需要在动画的时候生效。
  • 以上的操作参考了elementui的el-menu 和 iview里面的menu
  • 使用下面这种原生的方式实现之后,再对比vue的transition组件的的钩子函数感觉好类似阿(可参考:vue项目中实现折叠面板动画效果),就是不知道,我这样用setTimeout到底属不属于正常操作。不过,感觉理解了下面这个之后,再去看vue的transition过渡钩子好像就比较容易理解了
<style lang="scss" scoped>
@import url(//at.alicdn.com/t/c/font_4065865_kb7oyb2wje9.css);
ul,
li {
    margin: 0;
    list-style: none;
    padding: 0;
}

.tree-wrapper {
    width: 200px;
    border: 1px solid #ccc;
    border-radius: 5px;
    user-select: none;
}

.menu-title {
    padding: 7px 12px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: space-between;
    &:hover{
        background-color: #eee;
    }
    i.iconfont.icon-jiantou {
        font-size: 26px;
        display: inline-block;
        transition: transform 0.3s;
    }
}

// 箭头展开样式
.menu-opened > .menu-title > i.icon-jiantou {
    transform: rotate(180deg);
}


// 子菜单高度使用过渡
ul.menu {
    transition: all 0.3s;
    overflow: hidden;
}



</style>

<template>
    <div class="tree-wrapper">
        <ul class="menu">
            <li class="menu-submenu menu-opened" >
                <div class="menu-title" data-expanded="true" @click="clickMenu($event)">
                    <div>
                        <span>目录1</span>
                    </div>
                    <i class="iconfont icon-jiantou"></i>
                </div>
                <ul class="menu" id="t1-u">
                    <li class="menu-submenu menu-opened" >
                        <div class="menu-title" data-expanded="true" style="padding-left: 43px;"  @click="clickMenu($event)">
                            <div>
                                <span>
                                    目录1-1
                                </span>
                            </div>
                            <i class="iconfont icon-jiantou"></i>
                        </div>
                        <ul class="menu">
                            <li class="menu-item"  data-expanded="true">
                                <div class="menu-title" style="padding-left: 67px;">
                                    <div>
                                        <span>菜单1-1-1</span>
                                    </div>
                                </div>
                            </li>
                        </ul>
                    </li>
                    <li class="menu-item">
                        <div class="menu-title" style="padding-left: 43px;">
                            <div>
                                <span>菜单1-2</span>
                            </div>
                        </div>
                    </li>
                </ul>
            </li>
            <li class="menu-submenu menu-opened" >
                <div class="menu-title" data-expanded="true"  @click="clickMenu($event)">
                    <div>
                        <span>目录2</span>
                    </div>
                    <i class="iconfont icon-jiantou"></i>
                </div>
                <ul class="menu">
                    <li class="menu-item">
                        <div class="menu-title" style="padding-left: 43px;">
                            <div>
                                <span>菜单2-1</span>
                            </div>
                        </div>
                    </li>
                    <li class="menu-item">
                        <div class="menu-title" style="padding-left: 43px;">
                            <div>
                                <span>菜单2-2</span>
                            </div>
                        </div>
                    </li>
                </ul>
            </li>
            <li class="menu-item">
                <div class="menu-title">
                    <div>
                        <span>菜单4</span>
                    </div>
                </div>
            </li>
        </ul>
    </div>
</template>

<script setup>
    function clickMenu(e) {

        // console.log(e.target,'e.target'); // 获取的是发生事件的对象,有可能是子元素
        // console.log(e.currentTarget,'e.currentTarget'); // 获取的是绑定了事件的对象, 这里用的是这个!
        // console.log(e.currentTarget.dataset); // 自定义的dataset属性
        // console.log(e.currentTarget.nextSibling); // 下一个兄弟节点
        
        // 获取绑定了点击事件的对象, 即目录的那个menu-title这个dom
        let currentTarget = e.currentTarget

        // 使用dataset自定义属性, 将当前目录所对应的子节点是否为展开状态, 记录到data-expanded属性当中, 作为一个标记
        // 如果它是打开状态, 那么就需要关闭它
        if(currentTarget.dataset['expanded'] == 'true') {
            console.log(1);
            
            // 获取目录的下一个节点ul
            let ul  =  currentTarget.nextSibling

            // 移除掉父节点的menu-opened类(这个类用来控制三角形的旋转状态)
            ul.parentNode.classList.remove('menu-opened')

            // 在打开状态下,先去获取ul的scrollHeight值作为ul的height值(里面有个细节,如果ul中还有未展开的节点,那么此时获取ul的scrollHeight是不包括未展开节点的高度的)
            // 获取这个高度的目的是因为:
            //     1. 我们知道关闭的时候的高度是0,但是不知道打开状态下的高度是多少(不能是auto,写auto的话,高度是正常了,但是没有过渡动画),所以拿scrollHeight作为高度
            //     2. 我们一定要保持在动画完毕时, 高度要清理掉, 否则后面的动画无法继续下去。所以不能直接设置style.height,然后就不管了, 动画完成后要清理掉style.height。
            ul.style.height = ul.scrollHeight + 'px'

            // 这里的setTimeout不能省略, 虽然延迟时间为0。
            // 上面设置了起始高度,如果要产生过渡动画的话,那就要另一个高度值,关闭的时候,结束高度显然是0px,但是不能直接立马设置为0px,
            // 需要放在虾米那这个setTimeout里面去。
            setTimeout(()=>{
                console.log(ul);

                // 设置结束高度
                ul.style.height = '0px'

                const func = ()=>{

                    // 这里的意思就是想在动画结束后,把高度清空,然后将ul给隐藏掉,保持干净

                    // 动画都结束了,将ul隐藏掉
                    ul.style.display = 'none'

                    // 解绑事件函数
                    ul.removeEventListener('transitionend',func)

                    // 记录当前目录是关闭状态
                    currentTarget.dataset['expanded'] = 'false'
                    
                    // 将高度置为空(这个很重要,动画结束后,这个高度一定要清空掉,因为这个高度不能写死,
                    //             如果写死了,万一它里面还有子节点的话,子节点一旦展开,那这个高度肯定不够,
                    //             我们需要的只是在过渡的时候需要它的高度)
                    ul.style.height = null
                    
                    console.log(currentTarget.dataset['expanded'],123);
                }

                // 在动画结束后,直接func函数
                ul.addEventListener('transitionend', func)
            },0)
        } else {

            // 如果它是关闭状态, 那么就需要打开它
            // 打开它的话,就必须要知道它有多高,才能产生动画,实现0到指定高度的变化
            console.log(2);

            // 拿到目录标题dom的下一个节点ul
            let ul  =  currentTarget.nextSibling

            // 三角形打开状态
            ul.parentNode.classList.add('menu-opened')

            // 开始是0px(过渡的起始值)
            ul.style.height = '0px'

            // 可见状态
            ul.style.display = 'block'

            // 修改ul的高度必须要写在setTimeout里面,不能在setTimeout外面立马改掉
            setTimeout(()=>{

                // 设置过渡的结束值
                ul.style.height = ul.scrollHeight + 'px'

                const func = ()=>{

                    // 解除事件绑定
                    ul.removeEventListener('transitionend',func)

                    // 记录当前是打开状态
                    currentTarget.dataset['expanded'] = 'true'

                    // 将高度置为空(这个很重要,动画结束后,这个高度一定要清空掉,因为这个高度不能写死,
                    //             如果写死了,万一它里面还有子节点的话,子节点一旦展开,那这个高度肯定不够,
                    //             我们需要的只是在过渡的时候需要它的高度)
                    ul.style.height = null
                }

                // 动画结束后,收尾工作
                ul.addEventListener('transitionend', func)
            })
        }
    }
</script>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/522028.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue基础入门

1. vue简介 1.1 什么是vue 官方概念&#xff1a;Vue&#xff08;读音/vju:/&#xff0c;类似于view&#xff09;是一套用于构建用户界面的前端框架 1.2 vue 的特性 vue 框架的特性&#xff0c;主要体现在如下两方面&#xff1a; ① 数据驱动视图 ② 双向数据绑定 数据驱动…

IMS补充业务场景介绍

呼叫保持流程 通话主动Hold的一方,发INVITE消息,媒体流从sendrecv变为sendonly,对方返回200 ok,媒体流从sendrecv变为recvonly,双方ACK后,进入呼叫保持状态,没有通话的RTP包。 大致流程如下 UE A发送INVITE(Sendonly)到网络 网络发送INVITE(Sendonly)到UE B UE发…

Linux文件属性修改

关于我们的文件属性如何修改呢&#xff1f; 我们今天来看一下 chmod chmod u(拥有者)/g(所属组)/o(其他人)(-)r/w/x(t) 文件名 就是这样&#xff0c;我们演示几个 我们想给拥有者去掉file1的读权限 我们file1的拥有者已经没有读权限了&#xff0c;那么我们还想加回来呢…

asp.net+C#基于web的旅游网站自驾游网站

&#xff08;1&#xff09;登录注册模块&#xff1a;输入账号密码&#xff0c;数据库进行验证&#xff0c;正确通过后&#xff0c;根据不同的账户信息&#xff0c;不同角色&#xff0c;获取不同的功能。 &#xff08;2&#xff09;自驾游模块&#xff1a;此模块可以分享自己自…

《计算机网络—自顶向下方法》 第五章Wireshark实验:UDP 协议分析

用户数据报(UDP)协议是运输层提供的一种最低限度的复用/分解服务&#xff0c;可以在网络层和正确的用户即进程间传输数据。UDP 是一种不提供不必要服务的轻量级运输协议&#xff0c;除了复用/分用功能和简单的差错检测之外&#xff0c;几乎就是 IP 协议了&#xff0c;也可以说它…

Python操作Redis常见类型详解

1、windows 上安装 Redis 便于测试&#xff0c;笔者在 windows 上安装 Redis Redis 官方不建议在 windows 下使用 Redis&#xff0c;所以官网没有 windows 版本可以下载。微软团队维护了开源的 windows 版本&#xff0c;对于普通测试使用足够了。 1.1、安装包方式安装 Redis…

万字收藏:《2023网络工程师年度必看书单》

晚上好&#xff0c;我是老杨。 这周是总结周&#xff0c;更新的第三篇内容&#xff0c;还是关于总结的。很多人让我推荐网工适合看的书&#xff0c;其实我推荐过好多次了。 趁着年底&#xff0c;一起把我认为网工适合看的、推荐你看的、值得看的书整理一下&#xff0c;供新老…

视觉SLAM ch13 设计SLAM系统

目录 一、SLAM系统 二、工程框架 三、框架流程 四、具体实现 五、VO整体流程 六、显示整体建图效果 一、SLAM系统 实现一个精简版的双目视觉里程计&#xff0c;前端使用光流法&#xff0c;局部使用局部BA优化。 二、工程框架 app中 run_kitti_stereo.cpp是代码的运行入口…

国内免费可用 ChatGPT 网页版

ChatGPT是一个神奇的机器人&#xff0c;它可以回答任何问题&#xff0c;解决任何问题。它的名字来源于“Chat”和“GPT”&#xff0c;前者代表聊天&#xff0c;后者代表生成预测文本。它被设计成一个智能助手&#xff0c;可以帮助人们解决各种问题。 有一天&#xff0c;一个名…

【Python 爬虫之requests库】零基础也能轻松掌握的学习路线与参考资料

文章目录 一、概述二、Requests 库基本用法三、爬虫中的优秀实践四、参考资料 一、概述 Python 爬虫中&#xff0c;常用来请求网页的库有 urllib、urllib2、httplib等&#xff0c;但是这些库用起来比较麻烦&#xff0c;需要写很多代码。Requests 库正是为了解决这个问题而生的…

Flask轻松构建钉钉接口模版,实现自动化流程优化

项目背景 随着钉钉应用的不断普及和企业数字化程度的提高&#xff0c;越来越多的企业需要开发钉钉接口来完成内部业务流程的自动化和优化。而Flask框架&#xff0c;则是一个轻量级的Python web框架&#xff0c;具有快速开发和灵活性的优势&#xff0c;是钉钉接口开发的理想选择…

python去重列表中相同的字典元素

python去重列表中相同的字典元素 文章目录 python去重列表中相同的字典元素一.知识点二.代码|代码1|问题 |代码2 三.分析总结1、分析2、总结 四.后续代码知识点代码流程问题总结总结 一.知识点 ​ data_list [{“a”: 1, “b”: 2}, {“a”: 2, “b”: 3}, {“a”: 1, “b”:…

华为OD机试真题 Java 实现【相同数字的积木游戏1】【2023Q2 100分】

一、题目描述 小华和小薇一起通过玩积木游戏学习数学。 他们有很多积木&#xff0c;每个积木块上都有一个数字&#xff0c;积木块上的数字可能相同。 小华随机拿一些积木挨着排成一排&#xff0c;请小薇找到这排积木中数字相同且所处位置最远的2块积木块&#xff0c;计算他们…

【C++】——string的模拟实现

前言&#xff1a; 在之前的学习中&#xff0c;我们已经对string类进行了简单的介绍&#xff0c;大家只要能够正常使用即可。但是在面试中&#xff0c;面试官总喜欢让学生自己 来模拟实现string类&#xff0c;最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数…

lightroom磨皮滤镜中文插件Portraiture4最新版本

哈喽&#xff01;小伙伴们&#xff01;整个摄影后期行业都在用Portraiture&#xff0c;这是一个被奉为高级磨皮面板&#xff0c;修图神器、修图的的扩展面板&#xff01;Portraiture这款磨皮插件终于更新啦&#xff01;最近推出了Portraiture4.03版本,新版本光影处理更强大&…

《编程思维与实践》1066.最小不重复数

《编程思维与实践》1066.最小不重复数 题目 思路 一般在oj上循环 2 ⋅ 1 0 9 2\cdot 10^9 2⋅109次以上就会超时,所以由于这题的数据A可以很大,直接循环加一再判断会超时. 优化:首先可以明确要想使不重复数尽可能小,则高位数字应该尽可能小, 即先找到最靠前的两个重复数字,然后…

【Vector VN1630/40 I/O应用】-1-简易示波器

案例背景(共13页精简)&#xff1a;该篇博客将告诉您&#xff1a; Vector VN1630A&#xff0c;VN1640A&#xff0c;VH6501 I/O的使用&#xff1b;将Vector VN1630A/VN1640A CAN/LIN Interface的I/O接口充当一个简易的“示波器”使用&#xff1b;观察“CAN唤醒”工作的ECU控制器…

关于C语言的杂记4

文章目录 数据与程序结构C语言的编程机制#include <>和#include ""的区别形式参数和实际参数值传递地址传递 素数 文章内容摘自或加工于C技能树一些大佬的博文 数据与程序结构 阅读完C的编程机制和函数的声明和定义后的一些启发。——预处理 C语言的编程机制 …

dubbo技术

1、Dubbo的前世今生 2011年10月27日&#xff0c;阿里巴巴开源了自己的SOA服务化治理方案的核心框架Dubbo&#xff0c;服务治理和SOA的设计理念开始逐渐在国内软件行业中落地&#xff0c;并被广泛应用。 早期版本的dubbo遵循SOA的思想&#xff0c;是面向服务架构的重要组件。 …

1708_Simulink中取数组元素

全部学习汇总&#xff1a; GitHub - GreyZhang/g_matlab: MATLAB once used to be my daily tool. After many years when I go back and read my old learning notes I felt maybe I still need it in the future. So, start this repo to keep some of my old learning notes…