JS手写系列--手风琴菜单

0 阅读2分钟

什么是手风琴菜单?

  • 多个一级菜单,每个一级菜单下有多个二级菜单
  • 点击一级菜单,展开其二级菜单。再次点击,收起。
  • 最多只能有一个一级菜单展开。

HTML部分

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>手风琴效果</title>
    <link rel="stylesheet" href="/common/css/reset.css" />
    <link rel="stylesheet" href="./index.css" />
  </head>
  <body>
    <ul class="container">
      <li class="menu-item">
        <h1 class="menu-title">菜单1</h1>
        <ul class="submenu">
          <li>菜单1-1</li>
          <li>菜单1-2</li>
          <li>菜单1-3</li>
          <li>菜单1-4</li>
        </ul>
      </li>
      <li class="menu-item">
        <h1 class="menu-title">菜单2</h1>
        <ul class="submenu">
          <li>菜单2-1</li>
          <li>菜单2-2</li>
          <li>菜单2-3</li>
          <li>菜单2-4</li>
        </ul>
      </li>
      <li class="menu-item">
        <h1 class="menu-title">菜单3</h1>
        <ul class="submenu">
          <li>菜单3-1</li>
          <li>菜单3-2</li>
          <li>菜单3-3</li>
          <li>菜单3-4</li>
        </ul>
      </li>
      <li class="menu-item">
        <h1 class="menu-title">菜单4</h1>
        <ul class="submenu">
          <li>菜单4-1</li>
          <li>菜单4-2</li>
          <li>菜单4-3</li>
          <li>菜单4-4</li>
        </ul>
      </li>
    </ul>
    <script src="/common/js/animate.js"></script>
    <script src="./index.js"></script>
  </body>
</html>

CSS部分

.container {
  width: 200px;
  margin: 0 auto;
  margin-top: 100px;
}
/* 菜单项 */
.container .menu-item {
  overflow: hidden;
  background: rgb(50, 50, 50);
  color: #fff;
  line-height: 40px;
  margin-bottom: 10px;
  padding-left: 20px;
  border-radius: 6px;
}
/* 菜单title */
.container .menu-item .menu-title {
  height: 40px;
  line-height: 40px;
}
/* 子菜单容器 */
.container .menu-item .submenu {
  height: 0;
}
/* 子菜单项 */
.container .menu-item .submenu li {
  border-radius: 1px;
  font-size: 12px;
  height: 30px;
  line-height: 30px;
  padding-left: 10px;
}

JS部分

index.js
(function () {
  // 菜单标题dom集合
  var menu_titles = document.querySelectorAll('.container .menu-item .menu-title')
  console.log('menu_titles', menu_titles);

  // 子菜单dom集合
  var submenus = document.querySelectorAll('.submenu')

  var currentExpand = null

  //给每个子菜单设置初始状态:closed、opened、playing
  for (var i = 0; i < submenus.length; i++) {
    submenus[i].setAttribute('status', 'closed')
  }

  // 注册菜单标题点击事件
  for (var i = 0; i < menu_titles.length; i++) {
    (function () {
      const title = menu_titles[i];
      title.addEventListener('click', function () {
        var submenu = this.parentElement.querySelector('.submenu')
        var status = submenu.getAttribute('status')
        if (status === 'closed') {
          currentExpand && closeMenu(currentExpand)
          currentExpand = submenu
          openMenu(submenu)
        } else if (status === 'opened') {
          closeMenu(submenu)
        } else {
          return
        }
      })
    })(i)
  }
  function openMenu(submenu) {
    animate({
      from: 0,
      to: submenu.children[0].clientHeight * submenu.children.length,
      totalDuration: 200,
      singleDuration: 10,
      onAnimate: function (value) {
        submenu.style.height = value + 'px'
      },
      onEnd: function () {
        submenu.setAttribute('status', 'opened')
      }
    })

  }

  function closeMenu(submenu) {
    animate({
      from: submenu.clientHeight,
      to: 0,
      totalDuration: 200,
      singleDuration: 10,
      onAnimate: function (value) {
        submenu.style.height = value + 'px'
      },
      onEnd: function () {
        submenu.setAttribute('status', 'closed')
      }
    })
  }
})()
animate.js
// 动画的本质,是让某个数据从初始值逐渐变化到目标值,是让一个大变化拆分成很多个小步来执行,一次变化一点,从而形成连贯的感觉。
function animate(option) {
  var from = option.from //初始值
  var to = option.to //目标值
  var totalDuration = option.totalDuration || 500   //动画总持续时间
  var singleDuration = option.singleDuration || 10   //单步持续时间
  var onAnimate = option.onAnimate //单步动作内容(要干的事情)
  var onEnd = option.onEnd //动画结束回调
  
  var currentValue = from //当前值
  var totalSteps = Math.floor(totalDuration / singleDuration)   //总需步数
  var currentStep = 0   //当前步数
  var distance = to - from   //总变化值
  var singleStepDistance = distance / totalSteps   //单次变化值
  var timerId = setInterval(() => {
    currentStep++
    if (currentStep < totalSteps) {
      currentValue += singleStepDistance
    } else {
      currentValue = to
      clearInterval(timerId)
      onEnd()
    }
    onAnimate && onAnimate(currentValue)
  }, singleDuration);
}