七、JQuery实现带粘性侧边栏的列表,滚动时自动切换菜单(Desktop + Mobile ver.)

郁子原创大约 21 分钟约 6204 字JQuery

🍫 前言

如题,是刚刚完成的公司网站的一个简单小功能,抽离出来形成 Demo,锻炼写博客能力的第一步 💪。

1.在线尝试

CodeSandboxopen in new window

2.仓库地址

3.效果预览

AnimateEffect.gif

4.实现功能

🍩 顶部导航栏

  • 滚动时固定在页面最上方。

🍩 侧边菜单栏

  1. 一般状态下和右侧列表构成 flex 左右布局。
  2. 点击菜单滚动到右侧列表相应的红色标题上方。
  3. 滚动距离进入中间白色内容区时固定在侧边。
  4. 滚动距离离开中间白色内容区时恢复一般状态。
  5. 滚动距离进入内容区右侧列表红色标题时,自动切换到相应的菜单栏。

🍩 右侧列表区

  1. 鼠标经过问题时显示白色背景和阴影,问题展开时无此效果。
  2. 点击问题时切换内容展开/折叠状态。
  3. 对于同一个菜单下的问题,每次有且仅有一个被展开,即点击其他问题会折叠当前展开的问题。
  4. 同一个菜单下最多显示 5 个问题,超出的问题隐藏并显示 “Show more”。
  5. 点击 “Show more” 显示当前菜单下所有问题,点击 “Hide” 隐藏当前菜单下超出数量的问题。

(一)基础页面结构

🍩 仓库文件

见分支:initialopen in new window

🍩 页面效果

粘性侧边栏02.gif

🍩 布局

  • 整体采用上中下布局,中间渲染侧边栏和问题列表。
  • 红色区域用于撑开页面高度,更好演示滚动效果。
<div class="my-header">这是粘性顶部导航栏</div>
<div class="my-area">
  <p>这是一个区域</p>
</div>
<div class="my-menu-list">
  <div class="my-left-div">
    <div id="my_left_bar" class="my-left-bar"></div>
  </div>
  <div class="my-right-list">
    <div id="questions_list"></div>
  </div>
</div>
<div class="my-area">
  <p>这是一个区域</p>
</div>
<div class="my-area">
  <p>这是一个区域</p>
</div>
<div class="my-area">
  <p>这是一个区域</p>
</div>
<div class="my-footer">这是底部导航栏</div>

🍩 样式

/* container */
* {
  margin: 0;
  padding: 0;
  border: 0;
  outline: none;
}

body {
  background-color: #f0f2f5;
}

.my-header {
  width: 100%;
  height: 80px;
  background-color: #ffffff;
  box-shadow: 0px 16px 40px 0px rgba(112, 144, 176, 0.2);
  color: #303030;
  text-align: center;
  font-size: 20px;
  font-weight: bold;
  display: flex;
  align-items: center;
  justify-content: center;
  /* 滚动时固定在顶部 */
  position: sticky;
  top: 0;
  z-index: 9;
}

.my-area {
  width: 100%;
  height: 300px;
  background-color: #d8152a;
  color: #f0f2f5;
  margin: 0 auto;
  text-align: center;
  font-size: 20px;
  font-weight: bold;
  display: flex;
  align-items: center;
  justify-content: center;
}

.my-footer {
  width: 100%;
  height: 80px;
  background-color: #ffffff;
  color: #303030;
  text-align: center;
  font-size: 20px;
  font-weight: bold;
  display: flex;
  align-items: center;
  justify-content: center;
}

.my-menu-list {
  width: 1280px;
  height: 750px;
  margin: 100px auto;
  padding: 20px;
  display: flex;
  align-items: stretch;
  justify-content: flex-start;
  position: relative;
  overflow: auto;
}

/* left menu */
.my-left-div {
  width: 250px;
  position: relative;
}

.my-left-bar {
  width: 100%;
  height: 100%;
  background-color: lightgrey;
}

/* right list */
.my-right-list {
  flex: 1;
  padding: 0 30px;
  background-color: lightcoral;
}

(二)渲染左侧列表

🍩 仓库文件

见分支:leftbaropen in new window

🍩 页面效果

粘性侧边栏03.gif

1.准备菜单项图标

  • 可以将 UI 提供的图标文件放到项目目录下,以 img 形式引入。
  • 这里为了方便直接使用 iconfontopen in new window 提供的 CDN
  • 官方文档-使用帮助open in new window
  • 步骤如下:
    1. 选择心仪的图标添加到购物车。
    2. 点击顶部右侧的小车车图标,将选中的图标添加到项目中。
    3. 进入资源管理-我的项目菜单,选中上一步创建的项目,依次为图标重命名,选择 Font class 生成在线链接。
    4. 点击生成的在线链接会自动打开 css 样式代码,全选粘贴到 style.css 文件中。
    5. 页面中使用:<i class="iconfont icon-xxx"></i>
/* iconfont */
@font-face {
  font-family: "iconfont"; /* Project id 3867129 */
  src: url("//at.alicdn.com/t/c/font_3867129_yw1h2xfi3p.woff2?t=1674110301689")
      format("woff2"),
    url("//at.alicdn.com/t/c/font_3867129_yw1h2xfi3p.woff?t=1674110301689")
      format("woff"),
    url("//at.alicdn.com/t/c/font_3867129_yw1h2xfi3p.ttf?t=1674110301689")
      format("truetype");
}

.iconfont {
  font-family: "iconfont" !important;
  font-size: 16px;
  font-style: normal;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.icon-arrown-up:before {
  content: "\e616";
}

.icon-arrown-down:before {
  content: "\e600";
}

.icon-address:before {
  content: "\e608";
}

.icon-user:before {
  content: "\e60a";
}

.icon-form:before {
  content: "\e613";
}

.icon-notification:before {
  content: "\e614";
}

.icon-lib:before {
  content: "\e615";
}

/* container */
......

2.定义侧边栏菜单对象

  • id:菜单项编号,用于【点击切换菜单】事件。
  • content_id:菜单项对应的问题列表项,用于【点击滚动到相应列表】事件。
  • title:菜单项标题。
  • icon:菜单项图标。
  • is_active:菜单项激活状态,仅用于首次渲染时默认选中第一个菜单项。
/**
 * js/data.js
 */
// 侧边栏对象
const LEFT_BAR = [
  {
    id: "address",
    content_id: "address_content",
    title: "Address",
    icon: "address",
    is_active: true,
  },
  {
    id: "user",
    content_id: "user_content",
    title: "User",
    icon: "user",
    is_active: false,
  },
  {
    id: "form",
    content_id: "form_content",
    title: "Form",
    icon: "form",
    is_active: false,
  },
  {
    id: "notification",
    content_id: "notification_content",
    title: "Notification",
    icon: "notification",
    is_active: false,
  },
  {
    id: "lib",
    content_id: "lib_content",
    title: "Libruary",
    icon: "lib",
    is_active: false,
  },
];

index.html 页面中引入该对象:

<script src="js/data.js"></script>

3.引入 JQuery

<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>Fixed Side Bar List</title>
  <link rel="stylesheet" href="css/style.css" />
  <script src="js/jquery.min.js"></script>
</head>

4.渲染侧边栏

<script>
  // ========== 左侧菜单 ==========

  // 渲染问答模块左侧菜单
  function displayLeftBar() {
    let html = "";
    $("#my_left_bar").fadeOut(300);
    $("#my_left_bar").html("");
    LEFT_BAR.forEach((item) => {
      html += `
          <div id="${item.id}" class="my-left-item ${item.is_active ? "active" : ""}">
            <i class="iconfont icon-${item.icon} my-left-icon"></i>
            <span class="my-left-title">${item.title}</span>
          </div>`;
      if (item.id !== "lib") {
        html += `<div class="my-left-space"></div>`;
      }
    });
    $("#my_left_bar").html(html);
    $("#my_left_bar").fadeIn(300);
  }

  displayLeftBar();
</script>
.my-menu-list {
  width: 1280px;
  height: auto; /* 高度自动撑开 */
  margin: 100px auto;
  padding: 20px;
  display: flex;
  align-items: stretch;
  justify-content: flex-start;
  position: relative;
  overflow: auto;
}

/* left menu */
.my-left-div {
  /* 占位div,确保侧边栏固定时右侧列表能保持flex布局 */
  width: 250px;
  position: relative;
}

.my-left-bar {
  width: max-content;
}

.my-left-item {
  height: 50px;
  padding: 0 20px;
  display: flex;
  align-items: center;
  cursor: pointer;
  border-left: 2px solid #cecacb;
  transition: all linear 0.2s;
}

.my-left-icon {
  color: #898989;
  margin-right: 10px;
}

.my-left-title {
  color: #898989;
  font-size: 18px;
  font-weight: bold;
  text-align: left;
  flex: 1;
}

.my-left-space {
  /* 空菜单项,形成菜单左侧轨道 */
  height: 50px;
  border-left: 2px solid #cecacb;
}

.my-left-item.active {
  cursor: default;
  border-left: 2px solid #d8152a;
}

.my-left-item.active .my-left-title,
.my-left-item.active .my-left-icon {
  color: #d8152a;
}

5.侧边栏点击切换事件

  • 通过添加/删除 active 类实现菜单项的样式切换。
  • 渲染菜单时为所有菜单项绑定点击事件,获取到当前点击的菜单项 id
  • 封装一个函数 changeLeftBar ,传入当前点击的菜单项 id,先遍历所有菜单项去除 active 类,再根据当前点击的菜单项 id 修改菜单项类。
  • 如果图标是使用 img 渲染,可以多传一个 icon 参数,函数中修改 imgsrc 属性值,实现图标激活的效果。
// ========== 左侧菜单 ==========

// 侧边栏切换样式
function changeLeftBar(id) {
  // 遍历菜单项置灰
  $(".my-left-item").each((index, bar) => {
    let barImg = $(`#${bar.id} .my-left-icon`);
    if ($(bar).hasClass("active")) {
      $(bar).removeClass("active");
      barImg.removeClass("active");
    }
  });
  // 激活当前点击元素
  let el = $(`#${id}`);
  let img = $(`#${id} .my-left-icon`);
  if (el.hasClass("active")) {
    el.removeClass("active");
  } else {
    el.addClass("active");
  }
}

// 渲染问答模块左侧菜单
function displayLeftBar() {
  let html = "";
  $("#my_left_bar").fadeOut(300);
  $("#my_left_bar").html("");
  LEFT_BAR.forEach((item) => {
    html += `
      <div id="${item.id}" class="my-left-item ${item.is_active ? "active" : ""}" onclick="changeLeftBar('${item.id}')">
        <i class="iconfont icon-${item.icon} my-left-icon"></i>
        <span class="my-left-title">${item.title}</span>
      </div>`;
    if (item.id !== "lib") {
      html += `<div class="my-left-space"></div>`;
    }
  });
  $("#my_left_bar").html(html);
  $("#my_left_bar").fadeIn(300);
}

displayLeftBar();

(三)渲染右侧列表

🍩 仓库文件

见分支:rightlistopen in new window

🍩 页面效果

粘性侧边栏04.gif

1.定义问题列表对象

  • id:问题列表项,用于【点击滚动到相应列表】事件。
  • title:对应的菜单项标题。
  • questions:同一菜单下的问题列表数组。
    • id:问题编号。
    • title:问题标题。
    • contents:问题内容。
/**
 * js/data.js
 */
// 侧边栏对象
const LEFT_BAR = [
  // ...
];

// 问题列表对象
const RIGHT_LIST = [
  {
    id: "address_content",
    title: "Address",
    questions: [
      {
        id: "1",
        title: "What...?",
        contents: `
        <ol>
          <li>
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad.
          </li>
          <li>
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad.
          </li>
          <li>
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad.
          </li>
          <li>
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad.
          </li>
          <li>
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad.
          </li>
        </ol>`,
      },
      {
        id: "2",
        title: "Why...?",
        contents: `
        <p>
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad.
        </p>`,
      },
      {
        id: "3",
        title: "How...?",
        contents: `
        <p>
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad.
        </p>
        <p>
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad.
        </p>`,
      },
      {
        id: "4",
        title: "Which...?",
        contents: `
        <ul>
          <li>
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad.
          </li>
          <li>
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad.
          </li>
          <li>
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad.
          </li>
          <li>
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad.
          </li>
        </ul>`,
      },
      {
        id: "5",
        title: "How...?",
        contents: `
        <p>
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad.
        </p>
        <p>
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad.
        </p>`,
      },
      {
        id: "6",
        title: "How...?",
        contents: `
        <p>
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad.
        </p>
        <p>
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad.
        </p>`,
      },
      {
        id: "7",
        title: "How...?",
        contents: `
        <p>
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad.
        </p>
        <p>
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad.
        </p>`,
      },
    ],
  },
  {
    id: "user_content",
    title: "User",
    questions: [
      // ...
    ],
  },
  {
    id: "form_content",
    title: "Form",
    questions: [
      // ...
    ],
  },
  {
    id: "notification_content",
    title: "Notification",
    questions: [
      // ...
    ],
  },
  {
    id: "lib_content",
    title: "Libruary",
    questions: [
      // ...
    ],
  },
];

2.渲染问题列表

// ========== 右侧列表 ==========

// 渲染问答模块右侧列表
function displayRightList() {
  $("#questions_list").fadeOut(300);
  $("#questions_list").html("");
  RIGHT_LIST.forEach((list) => {
    let html = `
      <div class="my-right-item" id=${list.id}>
        <div class="my-right-title">${list.title}</div>`;
    list.questions.forEach((question) => {
      html += `
        <div class="my-right-content-div">
          <div class="my-right-content-title" id="${question.id}">
            <div class="my-right-content-question">
              ${question.title}
            </div>
            <i class="iconfont icon-arrown-down my-right-content-arrow"></i>
          </div>
          <div class="my-right-content">
            ${question.contents}
          </div>
        </div>`;
    });
    html += "</div>";
    $("#questions_list").append(html);
  });
  $("#questions_list").fadeIn(300);
}

displayRightList();
/* right list */
.my-right-list {
  flex: 1;
  padding: 0 30px;
}

.my-right-item {
  width: 100%;
  margin-bottom: 200px;
}

.my-right-title {
  font-size: 24px;
  font-weight: bold;
  color: #d8152a;
  text-align: left;
  padding-left: 15px;
  margin-bottom: 30px;
}

.my-right-content-div {
  width: 100%;
  margin-top: 50px;
}

.my-right-content-title {
  padding: 20px 15px;
  border-bottom: 2px solid #e0e0e0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  cursor: pointer;
}

/* 鼠标悬浮时显示背景颜色和阴影 */
.my-right-content-title:hover {
  background-color: #ffffff;
  box-shadow: 0px 16px 40px 0px rgba(112, 144, 176, 0.2);
  transition: all linear 0.2s;
}

/* 当前问题激活时鼠标悬浮不显示背景颜色和阴影 */
.my-right-content-title.active:hover {
  background-color: transparent;
  box-shadow: none;
}

.my-right-content-question {
  font-size: 18px;
  font-weight: bold;
  color: #303030;
  text-align: left;
  transition: all linear 0.2s;
}

.my-right-content-question.active {
  color: #d8152a;
}

.my-right-content-arrow {
  width: 14px;
  height: 14px;
  transform: rotate(0deg);
  transition: all linear 0.2s;
}

.my-right-content-arrow.active {
  transform: rotate(180deg);
}

/* 问题内容的样式 */
.my-right-content {
  padding: 15px;
  display: none;
}

.my-right-content ul,
.my-right-content ol {
  margin: 0;
  padding: 0 0 0 20px;
}

.my-right-content ul li,
.my-right-content ol li,
.my-right-content p {
  text-align: justify;
  text-justify: inter-word;
  font-size: 16px;
  color: #555555;
  line-height: 1.5;
  margin-bottom: 20px;
}

.my-right-content a {
  text-decoration: none;
  color: #d8152a;
}

.my-right-content a:hover {
  border-bottom: 1px solid #d8152a;
}

.my-right-content-paragraph {
  font-size: 18px;
  font-weight: bold;
  color: #303030;
  text-align: left;
  padding: 0 15px 15px;
}

3.列表项点击切换事件

  • 通过添加/删除 active 类实现列表项的样式切换。
  • 为所有列表项标题绑定点击事件,获取到当前点击的列表项 DOM 元素。
  • 封装一个函数 handleQuestionsChanges ,传入当前点击的列表项 DOM 元素,先遍历当前 DOM 元素所属菜单下的所有列表项,清除激活状态,再激活当前点击的列表项。
  • 封装一个函数 changeQuestionsActiveClass ,传入要切换激活状态的列表项 DOM 元素是否是当前点击的 DOM 元素,分别获取到 DOM 元素的子节点:问题标题、下拉图标、问题内容区域,去除 active 类,再为当前点击的列表项下的三个子节点添加 active 类。
// 修改问题激活样式
function changeQuestionsActiveClass(el, isCurrent = false) {
  let title = el.children(".my-right-content-question");
  let arrow = el.children(".my-right-content-arrow");
  let content = el.siblings(".my-right-content");
  if (el.hasClass("active")) {
    el.removeClass("active");
    title.removeClass("active");
    arrow.removeClass("active");
    content.slideUp(300);
  } else {
    if (isCurrent) {
      el.addClass("active");
      title.addClass("active");
      arrow.addClass("active");
      content.slideDown(300);
    }
  }
}

// 问答折叠
function handleQuestionsChanges(el) {
  // 清除当前类别下的激活问题
  let otherEl = el.parent(".my-right-content-div").siblings(".my-right-content-div").children(".my-right-content-title");
  changeQuestionsActiveClass(otherEl);
  // 激活当前点击的问题
  changeQuestionsActiveClass(el, true);
}

// 渲染问答模块右侧列表
function displayRightList() {
  $("#questions_list").fadeOut(300);
  $("#questions_list").html("");
  RIGHT_LIST.forEach((list) => {
    let html = `
      <div class="my-right-item" id=${list.id}>
        <div class="my-right-title">${list.title}</div>`;
    list.questions.forEach((question) => {
      html += `
        <div class="my-right-content-div">
          <div class="my-right-content-title" id="${question.id}">
            <div class="my-right-content-question">
              ${question.title}
            </div>
            <i class="iconfont icon-arrown-down my-right-content-arrow"></i>
          </div>
          <div class="my-right-content">
            ${question.contents}
          </div>
        </div>`;
    });
    html += "</div>";
    $("#questions_list").append(html);
  });
  $("#questions_list").fadeIn(300);
  // 点击问题折叠同类目下其他问题,再展开自身
  $(".my-right-content-title").click(function () {
    handleQuestionsChanges($(this));
  });
}

displayRightList();

(四)页面滚动时吸附侧边栏

🍩 仓库文件

见分支:fixedleftopen in new window

🍩 页面效果

粘性侧边栏05.gif

1.吸附样式

  • 定义两个样式,改变侧边栏 DOM 元素的 position 定位属性open in new window ,进入滚动区域时吸附,回到页面顶部或者离开滚动区域时恢复初始状态。
  • 侧边栏悬浮时,每一个菜单项高度改为以视口高度 vh 为单位,避免小屏幕菜单溢出屏幕。
/* 菜单正常样式 */
.my-sidebar-static {
  height: auto;
  position: static;
  transition: all linear 0.2s;
}

/* 菜单粘性样式 */
.my-sidebar-fixed {
  height: 90vh;
  position: fixed;
  top: 120px;
  transition: all linear 0.2s;
}

.faq-sidebar-fixed .faq-left-item {
  height: 5vh;
}

.faq-sidebar-fixed .faq-left-space {
  height: 5vh;
}

2.吸附侧边栏

  • 使用 addEventListener 监听页面滚动,获取到侧边栏 DOM 元素 sideBar视窗滚动距离 windowTop中间滚动区域顶部位置 divTop中间滚动区域底部位置(即该区域高度) divBottom 四个属性值。
// 页面滚动时吸附侧边栏
window.addEventListener("scroll", function () {
  let sideBar = $("#my_left_bar");
  let windowTop = $(window).scrollTop();
  let divTop = $(".my-menu-list").offset().top - 100;
  let divBottom = $(".my-menu-list").height();
  // 滚动到问题列表顶部时吸附
  if (windowTop >= divTop) {
    sideBar.removeClass("my-sidebar-static").addClass("my-sidebar-fixed");
  }
  // 回到页面顶部或滚动出问题列表底部时恢复
  if (windowTop < divTop || windowTop >= divBottom) {
    sideBar.removeClass("my-sidebar-fixed").addClass("my-sidebar-static");
  }
});

(五)切换菜单项

🍩 仓库文件

见分支:scrolledopen in new window

🍩 页面效果

粘性侧边栏06.gif

1.页面滚动效果

  • 封装一个函数,传入 DOM 元素和偏移量,滚动页面至元素顶部。
// 滚动动效
function scrollToElementTop(el, offset = 200) {
  $("html,body").animate(
    {
      scrollTop: el.offset().top - offset,
    },
    500,
  );
}

2.点击菜单项时切换

  • 修改 changeLeftBar 函数,增加传入参数 是否需要滚动 ,默认 true ,滚动列表切换菜单项时不需要自动滚动,否则页面会卡死。
// ========== 左侧菜单 ==========

// 侧边栏切换样式
function changeLeftBar(id, needScroll = true) {
  // 遍历菜单项置灰
  $(".my-left-item").each((index, bar) => {
    let barImg = $(`#${bar.id} .my-left-icon`);
    if ($(bar).hasClass("active")) {
      $(bar).removeClass("active");
      barImg.removeClass("active");
    }
  });
  // 激活当前点击元素
  let el = $(`#${id}`);
  let img = $(`#${id} .my-left-icon`);
  if (el.hasClass("active")) {
    el.removeClass("active");
  } else {
    el.addClass("active");
  }
  needScroll && scrollToElementTop($(`#${id}_content`));
}

3.页面滚动时切换

  • 定义一个空数组 menuTops,储存每个菜单项对象的属性值 以及 相应的问题列表标题顶部距离 offsetTop,最后在数组内存储中间滚动区域底部边界的顶部距离作占位元素,确保在遍历激活菜单项时可以定位到最后一个菜单。
  • 遍历 menuTops 数组,当视窗滚动距离 大于等于 当前遍历到的内容标题顶部距离,且小于 下一个遍历到的内容标题顶部距离 时,激活当前问题对应的左侧菜单项,即在哪个问题列表项内部区域滚动时就激活哪个菜单项。
// 页面滚动时吸附侧边栏
window.addEventListener("scroll", function () {
  let sideBar = $("#my_left_bar");
  let windowTop = $(window).scrollTop();
  let divTop = $(".my-menu-list").offset().top - 100;
  let divBottom = $(".my-menu-list").height();
  // 滚动到问题列表顶部时吸附
  if (windowTop >= divTop) {
    sideBar.removeClass("my-sidebar-static").addClass("my-sidebar-fixed");
  }
  // 回到页面顶部或滚动出问题列表底部时恢复
  if (windowTop < divTop || windowTop >= divBottom) {
    sideBar.removeClass("my-sidebar-fixed").addClass("my-sidebar-static");
  }
  // 滚动到问题标题顶部时激活相应侧边栏菜单项
  let menuTops = [];
  LEFT_BAR.forEach((menu) => {
    menuTops.push({
      ...menu,
      offsetTop: $(`#${menu.content_id}`)?.offset()?.top - 300,
    });
  });
  menuTops.push({
    id: "my_bottom",
    content_id: "my_bottom",
    offsetTop: divTop + divBottom,
  });
  for (let i = 0; i < menuTops.length - 1; i++) {
    if (windowTop >= menuTops[i].offsetTop && windowTop < menuTops[i + 1].offsetTop) {
      changeLeftBar(menuTops[i].id, false);
    }
  }
});














 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

(六)问题列表溢出隐藏

🍩 仓库文件

见分支:showmoreopen in new window

🍩 页面效果

粘性侧边栏07.gif

1.渲染问题列表时限制数量

  • 根据问题列表的每一个 questionid 判断当前问题序号,序号小于等于 5 正常渲染,大于 5 时最外层 div 附带 my-right-content-more-div 类,用于设置隐藏样式。
  • 问题总数超过 5 个时在渲染完问题列表后追加 “Show more” 和 “Hide” 文本按钮,用于点击切换。
// 渲染问答模块右侧列表
function displayRightList() {
  $("#questions_list").fadeOut(300);
  $("#questions_list").html("");
  RIGHT_LIST.forEach((list) => {
    let html = `
      <div class="my-right-item" id=${list.id}>
        <div class="my-right-title">${list.title}</div>`;
    list.questions.forEach((question) => {
      if (parseInt(question.id) <= 5) {
        // 前五个问题正常显示
        html += `
          <div class="my-right-content-div">
            <div class="my-right-content-title" id="${question.id}">
              <div class="my-right-content-question">
                ${question.title}
              </div>
              <i class="iconfont icon-arrown-down my-right-content-arrow"></i>
            </div>
            <div class="my-right-content">
              ${question.contents}
            </div>
          </div>`;
      } else {
        // 超过五个问题时带上my-right-content-more-div类
        html += `
          <div class="my-right-content-div my-right-content-more-div">
            <div class="my-right-content-title" id="${question.id}">
              <div class="my-right-content-question">
                ${question.title}
              </div>
              <i class="iconfont icon-arrown-down my-right-content-arrow"></i>
            </div>
            <div class="my-right-content">
              ${question.contents}
            </div>
          </div>`;
        if (parseInt(question.id) === list.questions.length) {
          // 超过五个问题时末尾追加Show more文本按钮
          html += `
          <div class="my-right-content-more">
            <span>Show more</span>
            <i class="iconfont icon-arrown-down my-right-content-arrow"></i>
          </div>`;
        }
      }
    });
    html += "</div>";
    $("#questions_list").append(html);
  });
  $("#questions_list").fadeIn(300);
  // 点击问题折叠同类目下其他问题,再展开自身
  $(".my-right-content-title").click(function () {
    handleQuestionsChanges($(this));
  });
}









 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 










2.点击显示/隐藏溢出问题

  • 获取到溢出的问题 DOM 元素,利用 JQuery 封装的 slideDownslideUp 函数控制元素显示/隐藏。
// 渲染问答模块右侧列表
function displayRightList() {
  $("#questions_list").fadeOut(300);
  $("#questions_list").html("");
  RIGHT_LIST.forEach((list) => {
    let html = `
      <div class="my-right-item" id=${list.id}>
        <div class="my-right-title">${list.title}</div>`;
    list.questions.forEach((question) => {
      if (parseInt(question.id) <= 5) {
        // 前五个问题正常显示
        html += `
          <div class="my-right-content-div">
            <div class="my-right-content-title" id="${question.id}">
              <div class="my-right-content-question">
                ${question.title}
              </div>
              <i class="iconfont icon-arrown-down my-right-content-arrow"></i>
            </div>
            <div class="my-right-content">
              ${question.contents}
            </div>
          </div>`;
      } else {
        // 超过五个问题时带上my-right-content-more-div类
        html += `
          <div class="my-right-content-div my-right-content-more-div">
            <div class="my-right-content-title" id="${question.id}">
              <div class="my-right-content-question">
                ${question.title}
              </div>
              <i class="iconfont icon-arrown-down my-right-content-arrow"></i>
            </div>
            <div class="my-right-content">
              ${question.contents}
            </div>
          </div>`;
        if (parseInt(question.id) === list.questions.length) {
          // 超过五个问题时末尾追加Show more文本按钮
          html += `
          <div class="my-right-content-more">
            <span>Show more</span>
            <i class="iconfont icon-arrown-down my-right-content-arrow"></i>
          </div>`;
        }
      }
    });
    html += "</div>";
    $("#questions_list").append(html);
  });
  $("#questions_list").fadeIn(300);
  // 点击问题折叠同类目下其他问题,再展开自身
  $(".my-right-content-title").click(function () {
    handleQuestionsChanges($(this));
  });
  // 点击 Show more / hide 时切换超过五个的问题显示状态
  $(".my-right-content-more").click(function () {
    let moreDiv = $(this).siblings(".my-right-content-more-div");
    let textSpan = $(this).children("span");
    let textIcon = $(this).children(".my-right-content-arrow");
    if (textSpan.html() === "Show more") {
      moreDiv.slideDown(300);
      textSpan.html("Hide");
      textIcon.removeClass("icon-arrown-down").addClass("icon-arrown-up");
    } else {
      moreDiv.slideUp(300);
      textSpan.html("Show more");
      textIcon.removeClass("icon-arrown-up").addClass("icon-arrown-down");
    }
  });
}























































 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

3.Show more / Hide 样式

.my-right-content-more-div {
  display: none;
}

.my-right-content-more {
  display: block;
  text-align: center;
  margin: 50px auto 0;
  cursor: pointer;
  width: fit-content;
  font-size: 14px;
  color: #d8152a;
}

(七)适配移动端

🍩 仓库文件

见分支:mobileopen in new window

🍩 页面效果

粘性侧边栏08.gif

1.使用@media 媒体查询定义不同样式

  • 768px 作为屏幕分界线。
  • 使用 vw 作为单位。
@media only screen and (max-width: 767px) {
  .my-menu-list {
    width: 100%;
    height: 100%;
    margin: 0 auto;
    padding: 0;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    position: relative;
    overflow: auto;
  }

  /* left menu */
  .my-left-div {
    width: 100%;
    margin: 0 auto 10vw;
    overflow-x: auto;
    position: relative;
  }

  /* 隐藏横向滚动条 */
  .my-left-div::-webkit-scrollbar,
  .my-sidebar-fixed::-webkit-scrollbar {
    display: none;
  }

  .my-left-bar {
    width: max-content;
    display: flex !important;
    align-items: center;
  }

  .show-head-bg {
    box-shadow: none !important;
  }

  .my-sidebar-fixed {
    z-index: 2;
    width: 100vw;
    height: auto;
    overflow-x: auto;
    position: fixed;
    top: 20vw;
    background-color: #ffffff;
    box-shadow: 0px 16px 40px 0px rgba(112, 144, 176, 0.2);
    transition: all linear 0.2s;
  }

  .my-sidebar-static {
    position: static;
    transition: all linear 0.2s;
  }

  .my-left-item {
    height: 12vw;
    margin: 0 5vw;
    padding: 5vw 1.5vw 0;
    display: flex;
    align-items: center;
    transition: all linear 0.5s;
    border-bottom: 1vw solid transparent;
    border-left: none;
  }

  .my-left-icon {
    margin-right: 1.5vw;
    font-size: 4vw;
  }

  .my-left-title {
    color: #898989;
    font-size: 4vw;
    font-weight: bold;
    text-align: left;
    flex: 1;
  }

  .my-left-space {
    display: none;
  }

  .my-left-item.active {
    border-bottom: 1vw solid #d8152a;
    border-left: none;
  }

  .my-left-item.active .my-left-title {
    color: #d8152a;
  }

  /* right list */
  .my-right-list {
    flex: 1;
    padding: 0 5vw;
  }

  .my-right-item {
    width: 100%;
    margin-bottom: 30vw;
  }

  .my-right-title {
    font-size: 5vw;
    font-weight: bold;
    color: #d8152a;
    text-align: left;
    padding-left: 3vw;
    margin-bottom: 10vw;
  }

  .my-right-content-div {
    width: 100%;
    margin-top: 5vw;
  }

  .my-right-content-more-div {
    display: none;
  }

  .my-right-content-more {
    display: block;
  }

  .my-right-content-title {
    padding: 3vw;
    border-bottom: 0.5vw solid #e0e0e0;
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
  }

  .my-right-content-title:hover {
    background-color: transparent;
    box-shadow: none;
    transition: none;
  }

  .my-right-content-question {
    font-size: 4vw;
    font-weight: bold;
    color: #303030;
    text-align: left;
    transition: all linear 0.2s;
  }

  .my-right-content-question.active {
    color: #d8152a;
  }

  .my-right-content-arrow {
    width: 4vw;
    height: 4vw;
    transform: rotate(0deg);
    transition: all linear 0.2s;
  }

  .my-right-content-arrow.active {
    transform: rotate(180deg);
  }

  .my-right-content {
    padding: 3vw;
    display: none;
  }

  .my-right-content ul,
  .my-right-content ol {
    margin: 0;
    padding: 0 0 0 3vw;
  }

  .my-right-content ul li,
  .my-right-content ol li,
  .my-right-content p {
    text-align: justify;
    text-justify: inter-word;
    font-size: 3vw;
    color: #555555;
    line-height: 1.5;
    margin-bottom: 5vw;
  }

  .my-right-content a {
    text-decoration: none;
    color: #d8152a;
  }

  .my-right-content a:hover {
    border-bottom: 0.5vw solid #d8152a;
  }

  .my-right-content-paragraph {
    font-size: 3vw;
    font-weight: bold;
    color: #303030;
    text-align: left;
    padding: 0 3vw 3vw;
  }

  .my-right-content-more {
    width: 100%;
    margin: 5vw 0;
    font-size: 3vw;
    color: #d8152a;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .my-right-content-more span {
    margin-right: 3vw;
  }

  .my-right-content-more .my-right-content-arrow {
    width: 3vw;
    height: 3vw;
    transform: rotate(0deg);
    transition: all linear 0.2s;
  }

  .my-right-content-more .my-right-content-arrow.active {
    transform: rotate(-180deg);
  }
}

2.判断当前视口是否为移动端

//判断设备
function isMobile() {
  return window.innerWidth < 768;
}

3.滚动时切换选中菜单项

  • 在原先的 menuTops 数组基础上,额外记录每一项菜单对应的左侧偏移量 offsetLeft ,表示横向滚动菜单内的每一项初始时距离视口左侧的距离。
  • 切换菜单时判断是否为移动设备,是则使用 JQuery 的 animate() 函数横向滚动到对应菜单项。
// 页面滚动时吸附侧边栏
window.addEventListener("scroll", function () {
  let sideBar = $("#my_left_bar");
  let windowTop = $(window).scrollTop();
  let divTop = $(".my-menu-list").offset().top - 100;
  let divBottom = $(".my-menu-list").height();
  // 滚动到问题列表顶部时吸附
  if (windowTop >= divTop) {
    sideBar.removeClass("my-sidebar-static").addClass("my-sidebar-fixed");
  }
  // 回到页面顶部或滚动出问题列表底部时恢复
  if (windowTop < divTop || windowTop >= divBottom) {
    sideBar.removeClass("my-sidebar-fixed").addClass("my-sidebar-static");
  }
  // 滚动到问题标题顶部时激活相应侧边栏菜单项
  let menuTops = [];
  LEFT_BAR.forEach((menu) => {
    menuTops.push({
      ...menu,
      offsetTop: $(`#${menu.content_id}`)?.offset()?.top - 300,
      offsetLeft: $(`#${menu.id}`)?.offset()?.left - 20,
    });
  });
  menuTops.push({
    id: "my_bottom",
    content_id: "my_bottom",
    offsetTop: divTop + divBottom,
    offsetLeft: 0,
  });
  for (let i = 0; i < menuTops.length - 1; i++) {
    if (windowTop >= menuTops[i].offsetTop && windowTop < menuTops[i + 1].offsetTop) {
      if (isMobile()) {
        $("#my_left_bar").animate(
          {
            scrollLeft: menuTops[i].offsetLeft - $("#my_left_bar").offset().left + $("#my_left_bar").scrollLeft(),
          },
          0,
        );
      }
      changeLeftBar(menuTops[i].id, false);
    }
  }
});




















 






 






 
 
 
 
 
 
 
 
 

🍫 尾记

  • 以上就是本篇文章全部内容,一个非常常见且十分简单的小功能,视觉样式来自公司 UI 小姐姐~
  • 一开始看到设计稿还担心实现很复杂,一步一步拆解再整理出来才发现都是很基础的交互逻辑。
  • 希望能帮助到有需要的小伙伴,有不足之处欢迎指出 😃
上次编辑于: