七、JQuery实现带粘性侧边栏的列表,滚动时自动切换菜单(Desktop + Mobile ver.)
原创大约 21 分钟约 6204 字
🍫 前言
如题,是刚刚完成的公司网站的一个简单小功能,抽离出来形成 Demo,锻炼写博客能力的第一步 💪。
1.在线尝试
2.仓库地址
- Gitee: https://gitee.com/yuziikuko/fixed-side-bar-list.git
- GitHub: https://github.com/yuziikuko/Fixed_SideBar_List.git
3.效果预览
4.实现功能
🍩 顶部导航栏
- 滚动时固定在页面最上方。
🍩 侧边菜单栏
- 一般状态下和右侧列表构成 flex 左右布局。
- 点击菜单滚动到右侧列表相应的红色标题上方。
- 滚动距离进入中间白色内容区时固定在侧边。
- 滚动距离离开中间白色内容区时恢复一般状态。
- 滚动距离进入内容区右侧列表红色标题时,自动切换到相应的菜单栏。
🍩 右侧列表区
- 鼠标经过问题时显示白色背景和阴影,问题展开时无此效果。
- 点击问题时切换内容展开/折叠状态。
- 对于同一个菜单下的问题,每次有且仅有一个被展开,即点击其他问题会折叠当前展开的问题。
- 同一个菜单下最多显示 5 个问题,超出的问题隐藏并显示 “Show more”。
- 点击 “Show more” 显示当前菜单下所有问题,点击 “Hide” 隐藏当前菜单下超出数量的问题。
(一)基础页面结构
🍩 仓库文件
🍩 页面效果
🍩 布局
- 整体采用上中下布局,中间渲染侧边栏和问题列表。
- 红色区域用于撑开页面高度,更好演示滚动效果。
<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>
🍩 样式
- 对顶部导航栏开启 sticky 粘性定位 使其不随页面滚动。
- 同时设置
z-index: 9;
置于顶层,否则滚动时下方元素会遮挡导航栏。
/* 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;
}
(二)渲染左侧列表
🍩 仓库文件
🍩 页面效果
1.准备菜单项图标
- 可以将 UI 提供的图标文件放到项目目录下,以
img
形式引入。 - 这里为了方便直接使用 iconfont 提供的
CDN
。 - 官方文档-使用帮助
- 步骤如下:
- 选择心仪的图标添加到购物车。
- 点击顶部右侧的小车车图标,将选中的图标添加到项目中。
- 进入资源管理-我的项目菜单,选中上一步创建的项目,依次为图标重命名,选择
Font class
生成在线链接。 - 点击生成的在线链接会自动打开 css 样式代码,全选粘贴到
style.css
文件中。 - 页面中使用:
<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
参数,函数中修改img
的src
属性值,实现图标激活的效果。
// ========== 左侧菜单 ==========
// 侧边栏切换样式
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();
(三)渲染右侧列表
🍩 仓库文件
🍩 页面效果
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();
(四)页面滚动时吸附侧边栏
🍩 仓库文件
🍩 页面效果
1.吸附样式
- 定义两个样式,改变侧边栏 DOM 元素的 position 定位属性 ,进入滚动区域时吸附,回到页面顶部或者离开滚动区域时恢复初始状态。
- 侧边栏悬浮时,每一个菜单项高度改为以视口高度
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");
}
});
(五)切换菜单项
🍩 仓库文件
🍩 页面效果
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);
}
}
});
(六)问题列表溢出隐藏
🍩 仓库文件
🍩 页面效果
1.渲染问题列表时限制数量
- 根据问题列表的每一个
question
的id
判断当前问题序号,序号小于等于 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 封装的
slideDown
和slideUp
函数控制元素显示/隐藏。
// 渲染问答模块右侧列表
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;
}
(七)适配移动端
🍩 仓库文件
🍩 页面效果
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 小姐姐~
- 一开始看到设计稿还担心实现很复杂,一步一步拆解再整理出来才发现都是很基础的交互逻辑。
- 希望能帮助到有需要的小伙伴,有不足之处欢迎指出 😃