原生js实现自定义滚动条组件


Posted in Javascript onJanuary 20, 2021

本文实例为大家分享了js实现自定义滚动条组件的具体代码,供大家参考,具体内容如下

功能需求:

1、按照数据结构创建菜单内容,显示在页面中;
2、点击菜单后,显示对应的下级菜单内容,如果整体内容溢出,则出现滚动条;
3、滚动条的高度要随着整体内容高度的改变而改变。
4、鼠标拖动滚动条,整体内容要随着向上滚动。
5、当鼠标滚动时,滚动条和整体内容也要相应滚动。

来看一下效果:

默认状态:

原生js实现自定义滚动条组件

点击菜单,内容溢出后,出现滚动条;

原生js实现自定义滚动条组件

鼠标拖动滚动条,整体内容随着向上滚动:

原生js实现自定义滚动条组件

分析:

  • 这个案例中包括折叠菜单和滚动条两个组件 ,所以可以分开来写,然后整合到一起。
  • 折叠菜单中要考虑多级菜单出现的情况,使用递归来做,数据的结构一定要统一,方便对数据进行处理。
  • 滚动条的创建中,有两个比例等式,一是滚动条的高度/外层div高度=外层div高度/整体内容高度;二是滚动条的位置/(外层div高度-滚动条高度)=内容的scrollTop/(整体内容的高度-外层div高度)
  • 当点击折叠菜单后,需要相应地设置滚动条的高度。折叠菜单是在Menu.js文件中,滚动条的设置是在ScrollBar.js文件中,需要进行抛发、监听事件。
  • 监听菜单鼠标滚动的事件,当鼠标滚动时,判断滚轮方向,设置滚动条和内容的 top 值,也需要用到事件的抛发和监听。

下面附上代码:

html结构,模拟数据,创建外层容器:

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>scrollBar</title>
</head>
<body>
 <script type="module">
 import Utils from './js/Utils.js';
 import Menu from './js/Menu.js';
 import ScrollBar from './js/ScrollBar.js';
 var arr=[
  {name:"A",category:[
  {name:"奥迪",category:[
   {name:"奥迪A3",href:""},
   {name:"奥迪A4L",category:[
   {name:"奥迪A4L-1",href:""}
   ]},
   {name:"奥迪Q3",href:""},
   {name:"奥迪Q5L",href:""},
   {name:"奥迪Q2L",href:""},
   {name:"奥迪Q7(进口)",href:""},
   {name:"奥迪Q8(进口)",href:""},
   {name:"奥迪Q7新能源",href:""},
  ]},
  {name:"阿尔法-罗密欧",category:[
   {name:"Stelvio(进口)",href:""},
   {name:"Giulia(进口)",href:""},
  ]}
  ]},
  {name:"B",category:[
  {name:"奔驰",category:[
   {name:"奔驰C级",href:""},
   {name:"奔驰E级",href:""},
   {name:"奔驰GLA级",href:""},
   {name:"奔驰GLC级",href:""},
   {name:"奔驰A级",href:""},
   {name:"奔驰E级(进口)",href:""},
   {name:"奔驰A级(进口)",href:""},
   {name:"奔驰B级(进口)",href:""},
   {name:"威霆",href:""},
   {name:"奔驰V级",href:""},
  ]},
  {name:"宝马",category:[
   {name:"宝马5系",href:""},
   {name:"宝马1系",href:""},
   {name:"宝马X1",href:""},
   {name:"宝马X5(进口)",href:""},
   {name:"宝马X6(进口)",href:""},
  ]},
  {name:"本田",category:[
   {name:"竞瑞",href:""},
   {name:"思域",href:""},
   {name:"本田CR-V",href:""},
   {name:"本田XR-V",href:""},
   {name:"本田UR-V",href:""},
   {name:"艾力绅",href:""},
   {name:"享域",href:""},
   {name:"INSPIRE",href:""},
   {name:"凌派",href:""},
   {name:"雅阁",href:""},
   {name:"缤智",href:""},
  ]},
  {name:"别克",category:[
   {name:"凯越",href:""},
   {name:"英朗",href:""},
   {name:"威朗",href:""},
   {name:"阅朗",href:""},
   {name:"君威",href:""},
   {name:"君越",href:""},
   {name:"昂科拉",href:""},
   {name:"昂科威",href:""},
   {name:"别克GL8",href:""},
   {name:"别克GL6",href:""},
   {name:"VELITE",href:""},
  ]}
  ]}
 ]
 var container;
 init();
 function init(){
  createMenu(arr);  
  createScrollBar();
 }
  function createMenu(arr){
  //创建菜单
  let menu=new Menu(arr);
  //创建最外层容器
  container=Utils.createE("div",{
  width:"235px",
  height:"360px",
  border:"1px solid #ccc",
  position:"relative",
  overflow:"hidden"
  })
  menu.appendTo(container);
  Utils.appendTo(container,"body")
 }
 function createScrollBar(){
  //创建滚动条
  let scrollBar=new ScrollBar(container);
  scrollBar.appendTo(container);
 }
 </script>
</body>
</html>

Menu.js文件,根据数据创建折叠菜单内容:

import Utils from './Utils.js';
export default class Menu{
 static SET_BAR_HEIGHT="set_bar_height";
 static MOUSE_WHEEL_EVENT="mouse_wheel_event";
 constructor(_list){
 this.elem=this.createElem(_list);
 }
 createElem(_list){
 if(this.elem) return this.elem;
 //创建最外层ul容器
 let ul=Utils.createE("ul",{
  listStyle:"none",
  padding:"0px",
  margin:"0px",
  width:"235px",
  height:"360px",
  color:"#333",
  fontSize:"14px",
  userSelect: "none",
  position:"absolute"
 });
 //创建li列表
 this.createMenu(_list,ul);
 //ul监听点击事件
 ul.addEventListener("click",e=>this.clickHandler(e));
 //ul监听滚轮事件,火狐使用DOMMouseScroll,其它浏览器使用mousewheel
 ul.addEventListener("mousewheel",e=>this.mouseWheelHandler(e));
 ul.addEventListener("DOMMouseScroll",e=>this.mouseWheelHandler(e));
 return ul;
 }
 appendTo(parent){
 Utils.appendTo(this.elem,parent);
 }
 //创建一级菜单
 createMenu(_list,parent){
 for(let i=0;i<_list.length;i++){
  let li=Utils.createE("li",{
  background:"#f5f5f5",
  borderTop:"1px solid #ddd",
  lineHeight:"32px",
  },{
  data:1,//控制一级菜单不能点击折叠
  })
  let span=Utils.createE("span",{
  marginLeft:"14px",
  fontSize:"18px"
  },{
  textContent:_list[i].name
  })
  Utils.appendTo(span,li);
  Utils.appendTo(li,parent);
  //创建子菜单,第三个参数控制子菜单是否显示
  this.createSubMenu(_list[i].category,li,0);
 }
 }
 //创建子菜单
 createSubMenu(_subList,_parent,_index){
 //如果没有子菜单,则跳出
 if(_subList.length===0) return;
 let subUl=Utils.createE("ul",{
  listStyle:"none",
  background:"#fff",
  padding:"0px",
  margin:"0px",
  fontSize:"14px",
  display:_index===0? "block" : "none"
 })
 for(let i=0;i<_subList.length;i++){
  let subLi=Utils.createE("li",{
  paddingLeft:"40px",
  position:"relative",
  cursor:"pointer"
  })
  if(!_subList[i].category){
  //如果当前菜单没有子菜单,则创建a标签,进行跳转
  let subA=Utils.createE("a",{
   color:"#333",
   textDecoration:"none",
   width:"100%",
   display:"inline-block"
  },{
   textContent:_subList[i].name,
   href:_subList[i].href || "javascript:void(0)",
   target:_subList[i].href ? "_blank" : "_self"
  })
  Utils.appendTo(subA,subLi);
  }else{
  //如果当前菜单有子菜单,创建span标签
  let subSpan=Utils.createE("span",{
   position:"absolute",
   left:"20px",
   top:"8px",
   border: "1px solid #ccc",
   display: "inline-block",
   width: "10px",
   height: "10px",
   lineHeight:"8px"
  },{
   textContent:_subList[i].category.length>0? "+" : "-"
  })
  subLi.textContent=_subList[i].name;
  Utils.appendTo(subSpan,subLi);
  }
  Utils.appendTo(subLi,subUl);
  //如果当前菜单没有子菜单,则跳过下面的执行
  if(!_subList[i].category) continue;
  //将当前菜单的子菜单作为参数,进行递归
  this.createSubMenu(_subList[i].category,subLi,1);
 }
 Utils.appendTo(subUl,_parent);
 }
 clickHandler(e){
 //如果当前点击的不是li标签或者span,直接跳出
 if(e.target.nodeName!=="LI" && e.target.nodeName!=="SPAN") return;
 let targ;
 if(e.target.nodeName==="SPAN") targ=e.target.parentElement;
 else targ=e.target;
 //如果当前点击Li下面没有子菜单,直接跳出
 if(targ.children.length<=1) return;
 //如果当前点击的是一级菜单,直接跳出
 if(targ.data===1) return;
 //控制当前点击的Li下的ul显示隐藏
 if(!targ.bool) targ.lastElementChild.style.display="block";
 else targ.lastElementChild.style.display="none";
 targ.bool=!targ.bool;
 //改变span标签的内容
 this.changeSpan(targ);
 //抛发事件,改变滚动条的高度
 var evt=new Event(Menu.SET_BAR_HEIGHT);
 document.dispatchEvent(evt)
 }
 changeSpan(elem){
 if(elem.lastElementChild.style.display==="block"){
  elem.firstElementChild.textContent="-";
 }else{
  elem.firstElementChild.textContent="+";
 }
 }
 mouseWheelHandler(e){
 //阻止事件冒泡
 e.stopPropagation();
 //火狐浏览器判断e.detail,e.detail<0时,表示滚轮往下,页面往上
 let tag=e.detail,wheelDir;
 //其他浏览器判断e.deltaY,e.deltaY<0时,表示滚轮往下,页面往上
 if(tag===0) tag=e.deltaY;

 if(tag>0){
  //滚轮往下滚动,页面往上走
  wheelDir="down";
 }else{
  wheelDir="up";
 }
 //抛发事件,将滚轮方向传递过去
 let evt=new Event(Menu.MOUSE_WHEEL_EVENT);
 evt.wheelDirection=wheelDir;
 this.elem.dispatchEvent(evt);
 }
}

ScrollBar.js文件,创建滚动条,对滚动条进行操作:

import Utils from './Utils.js';
import Menu from './Menu.js';
export default class ScrollBar {
 bar;
 conHeight;
 menuHeight;
 wheelSpeed=6;
 barTop=0;
 static SET_BAR_HEIGHT="set_bar_height";
 constructor(parent) {
 this.container = parent;
 this.menuUl=this.container.firstElementChild;
 this.elem = this.createElem();
 //侦听菜单的点击事件,动态改变滚动条的高度
 document.addEventListener(ScrollBar.SET_BAR_HEIGHT,()=>this.setBarHeight());
 //ul菜单侦听滚轮事件
 this.menuUl.addEventListener(Menu.MOUSE_WHEEL_EVENT,e=>this.mouseWheelHandler(e));
 }
 createElem() {
 if (this.elem) return this.elem;
 //创建滚动条的外层容器
 let div = Utils.createE("div", {
  width: "8px",
  height: "100%",
  position: "absolute",
  right: "0px",
  top: "0px",
 })
 this.createBar(div);
 return div;
 }
 appendTo(parent) {
 Utils.appendTo(this.elem,parent);
 }
 createBar(_parent) {
 if(this.bar) return this.bar;
 //创建滚动条
 this.bar = Utils.createE("div", {
  width: "100%",
  position: "absolute",
  left: "0px",
  top: "0px",
  borderRadius: "10px",
  backgroundColor: "rgba(255,0,0,.5)"
 })
 //设置滚动条hover状态的样式
 this.bar.addEventListener("mouseenter",e=>this.setMouseStateHandler(e));
 this.bar.addEventListener("mouseleave",e=>this.setMouseStateHandler(e));
 //设置滚动条的高度
 this.setBarHeight();
 //侦听鼠标拖动事件
 this.mouseHand = e => this.mouseHandler(e);
 this.bar.addEventListener("mousedown", this.mouseHand);
 Utils.appendTo(this.bar, _parent);
 }
 setBarHeight() {
 //外层父容器的高度
 this.conHeight = this.container.clientHeight;
 //实际内容的高度
 this.menuHeight = this.container.firstElementChild.scrollHeight;
 //如果实际内容的高度小于父容器的高度,滚动条隐藏
 if (this.conHeight >= this.menuHeight) this.bar.style.display = "none";
 else this.bar.style.display = "block";
 //计算滚动条的高度
 let h = Math.floor(this.conHeight / this.menuHeight * this.conHeight);
 this.bar.style.height = h + "px";
 }
 setMouseStateHandler(e){
 //设置滚动条hover状态的样式
 if(e.type==="mouseenter"){
  this.bar.style.backgroundColor="rgba(255,0,0,1)";
 }else{
  this.bar.style.backgroundColor="rgba(255,0,0,.5)";
 }
 }
 mouseHandler(e) {
 switch (e.type) {
  case "mousedown":
  e.preventDefault();
  this.y = e.offsetY;
  document.addEventListener("mousemove", this.mouseHand);
  document.addEventListener("mouseup", this.mouseHand);
  break;
  case "mousemove":
  //注意:getBoundingClientRect()返回的结果中,width height 都是包含border的
  var rect = this.container.getBoundingClientRect();
  this.barTop = e.clientY - rect.y - this.y;
  //滚动条移动
  this.barMove();
  break;
  case "mouseup":
  document.removeEventListener("mousemove", this.mouseHand);
  document.removeEventListener("mouseup", this.mouseHand);
  break;
 }
 }
 mouseWheelHandler(e){
 //滚轮事件
 if(e.wheelDirection==="down"){
  //滚动往下,菜单内容往上
  this.barTop+=this.wheelSpeed;
 }else{
  this.barTop-=this.wheelSpeed;
 }
 //滚动条移动
 this.barMove();
 }
 barMove(){
 if (this.barTop < 0) this.barTop = 0;
 if (this.barTop > this.conHeight - this.bar.offsetHeight) this.barTop = this.conHeight - this.bar.offsetHeight;
 this.bar.style.top = this.barTop + "px";
 //菜单内容滚动
 this.menuMove();
 }
 menuMove(){
 //计算内容的滚动高度
 let menuTop=this.barTop/(this.conHeight-this.bar.offsetHeight)*(this.menuHeight-this.conHeight);
 this.menuUl.style.top=-menuTop+"px";
 }
}

Utils.js文件,是一个工具包:

export default class Utils{
 static createE(elem,style,prep){
 elem=document.createElement(elem);
 if(style) for(let prop in style) elem.style[prop]=style[prop];
 if(prep) for(let prop in prep) elem[prop]=prep[prop];
 return elem;
 }
 static appendTo(elem,parent){
 if (parent.constructor === String) parent = document.querySelector(parent);
 parent.appendChild(elem);
 }
 static randomNum(min,max){
 return Math.floor(Math.random*(max-min)+min);
 }
 static randomColor(alpha){
 alpha=alpha||Math.random().toFixed(1);
 if(isNaN(alpha)) alpha=1;
 if(alpha>1) alpha=1;
 if(alpha<0) alpha=0;
 let col="rgba(";
 for(let i=0;i<3;i++){
  col+=Utils.randomNum(0,256)+",";
 }
 col+=alpha+")";
 return col;
 }
 static insertCss(select,styles){
 if(document.styleSheets.length===0){
  let styleS=Utils.createE("style");
  Utils.appendTo(styleS,document.head);
 }
 let styleSheet=document.styleSheets[document.styleSheets.length-1];
 let str=select+"{";
 for(var prop in styles){
  str+=prop.replace(/[A-Z]/g,function(item){
  return "-"+item.toLocaleLowerCase();
  })+":"+styles[prop]+";";
 }
 str+="}"
 styleSheet.insertRule(str,styleSheet.cssRules.length);
 }
 static getIdElem(elem,obj){
 if(elem.id) obj[elem.id]=elem;
 if(elem.children.length===0) return obj;
 for(let i=0;i<elem.children.length;i++){
  Utils.getIdElem(elem.children[i],obj);
 }
 }
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
自适应高度框架 ----属个人收藏内容
Jan 22 Javascript
javascript基本语法分析说明
Jun 15 Javascript
js 有框架页面跳转(target)三种情况下的应用
Apr 09 Javascript
js获取GridView中行数据的两种方法 分享
Jul 13 Javascript
jQuery中的val()示例应用
Feb 26 Javascript
JS递归遍历对象获得Value值方法技巧
Jun 14 Javascript
js仿腾讯QQ的web登陆界面
Aug 19 Javascript
JavaScript图片处理与合成总结
Mar 04 Javascript
Javascript 编码约定(编码规范)
Mar 11 Javascript
解决axios发送post请求返回400状态码的问题
Aug 11 Javascript
Vue发布项目实例讲解
Jul 17 Javascript
JS数组属性去重并校验重复数据
Jan 10 Javascript
原生js实现自定义滚动条
Jan 20 #Javascript
uniapp微信小程序:key失效的解决方法
Jan 20 #Javascript
JavaScript实现下拉列表
Jan 20 #Javascript
浅谈Vue开发人员的7个最好的VSCode扩展
Jan 20 #Vue.js
详解实现vue的数据响应式原理
Jan 20 #Vue.js
vue实现简易计算器功能
Jan 20 #Vue.js
vue使用过滤器格式化日期
Jan 20 #Vue.js
You might like
ThinkPHP自动转义存储富文本编辑器内容导致读取出错的解决方法
2014/08/08 PHP
php函数serialize()与unserialize()用法实例
2014/11/06 PHP
WordPress后台中实现图片上传功能的实例讲解
2016/01/11 PHP
使用jQuery的ajax功能实现的RSS Reader 代码
2009/09/03 Javascript
为jQuery增加join方法的实现代码
2010/11/28 Javascript
javascript不同类型数据之间的运算的转换方法
2014/02/13 Javascript
Jquery实现自定义弹窗示例
2014/03/12 Javascript
JavaScript中Math.SQRT2属性的使用详解
2015/06/14 Javascript
JS实现向iframe中表单传值的方法
2017/03/24 Javascript
React复制到剪贴板的示例代码
2017/08/22 Javascript
webpack4 SplitChunks实现代码分隔详解
2019/05/23 Javascript
JS通过ajax + 多列布局 + 自动加载实现瀑布流效果
2019/05/30 Javascript
Vue3 中的数据侦测的实现
2019/10/09 Javascript
JavaScript中的类型检查
2020/02/03 Javascript
关于vue属性使用和不使用冒号的区别说明
2020/10/22 Javascript
夯基础之手撕javascript继承详解
2020/11/09 Javascript
[01:18:21]EG vs TNC Supermajor小组赛B组败者组第一轮 BO3 第一场 6.2
2018/06/03 DOTA
python使用自定义user-agent抓取网页的方法
2015/04/15 Python
python实现在sqlite动态创建表的方法
2015/05/08 Python
Python基于pyCUDA实现GPU加速并行计算功能入门教程
2018/06/19 Python
详解Python解决抓取内容乱码问题(decode和encode解码)
2019/03/29 Python
python实现批量修改服务器密码的方法
2019/08/13 Python
python时间日期操作方法实例小结
2020/02/06 Python
python爬虫开发之urllib模块详细使用方法与实例全解
2020/03/09 Python
python中sklearn的pipeline模块实例详解
2020/05/21 Python
python写文件时覆盖原来的实例方法
2020/07/22 Python
python线程池 ThreadPoolExecutor 的用法示例
2020/10/10 Python
Swanson中国官网:美国斯旺森健康产品公司
2021/03/01 全球购物
办公室主任职责范文
2013/11/08 职场文书
法制宣传日活动总结
2014/04/29 职场文书
2014年社区工会工作总结
2014/12/18 职场文书
诚信承诺书
2015/01/19 职场文书
消防演习通知
2015/04/25 职场文书
公司员工违法违章行为检讨书
2019/06/24 职场文书
HR必备:销售经理聘用合同范本
2019/08/21 职场文书
golang定时器
2022/04/14 Golang