原生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 相关文章推荐
JavaScript Event学习第四章 传统的事件注册模型
Feb 07 Javascript
如何确保JavaScript的执行顺序 之实战篇
Mar 03 Javascript
『jQuery』.html(),.text()和.val()的概述及使用
Apr 22 Javascript
js实现div的切换特效上一个下一个
Feb 11 Javascript
Node.js中使用事件发射器模式实现事件绑定详解
Aug 15 Javascript
angularJS与bootstrap结合实现动态加载弹出提示内容
Oct 16 Javascript
灵活的理解JavaScript中的this指向
Feb 25 Javascript
jQuery根据表单name获取值的方法
May 24 Javascript
JS处理数据四舍五入(tofixed与round的区别详解)
Oct 26 Javascript
javascript实现贪吃蛇经典游戏
Apr 10 Javascript
在vue中实现清除echarts上次保留的数据(亲测有效)
Sep 09 Javascript
详解如何使用Node.js实现热重载页面
May 06 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
dede3.1分页文字采集过滤规则详说(图文教程)
2007/04/03 PHP
mayfish 数据入库验证代码
2010/04/30 PHP
php抓取页面的几种方法详解
2013/06/17 PHP
浅谈php扩展imagick
2014/06/02 PHP
php面向对象重点知识分享
2019/09/27 PHP
基于Jquery的温度计动画效果
2010/06/18 Javascript
基于jQuery的计算文本框字数的代码
2012/06/06 Javascript
Nodejs中session的简单使用及通过session实现身份验证的方法
2016/02/04 NodeJs
浅谈React Native 中组件的生命周期
2017/09/08 Javascript
vue-router history模式下的微信分享小结
2018/07/05 Javascript
微信小程序自定义对话框弹出和隐藏动画
2018/07/19 Javascript
vue项目打包后怎样优雅的解决跨域
2019/05/26 Javascript
微信小程序pinker组件使用实现自动相减日期
2020/05/07 Javascript
jquery实现简单自动轮播图效果
2020/07/29 jQuery
jQuery带控制按钮轮播图插件
2020/07/31 jQuery
在vscode 中设置 vue模板内容的方法
2020/09/02 Javascript
Python单例模式实例分析
2015/01/14 Python
Python时间获取及转换知识汇总
2017/01/11 Python
Python标准库sched模块使用指南
2017/07/06 Python
python机器学习理论与实战(五)支持向量机
2018/01/19 Python
Python matplotlib绘制饼状图功能示例
2019/09/10 Python
Python学习笔记之装饰器
2020/08/06 Python
基于Django集成CAS实现流程详解
2020/11/28 Python
python 实现图片批量压缩的示例
2020/12/18 Python
基于CSS3实现图片模糊过滤效果
2015/11/19 HTML / CSS
中国领先的专业演出票务网:永乐票务
2016/08/29 全球购物
Roots加拿大官网:加拿大休闲服饰品牌
2016/10/24 全球购物
澳大利亚最早和最古老的巨型游戏专家:Yardgames
2020/02/20 全球购物
实习求职信
2013/12/01 职场文书
可贵的沉默教学反思
2014/02/06 职场文书
跟单业务员岗位职责
2014/03/08 职场文书
司机岗位职责说明书
2014/07/29 职场文书
化学专业大学生职业生涯规划范文
2014/09/13 职场文书
《黄山奇石》教学反思
2016/02/18 职场文书
讲解Python实例练习逆序输出字符串
2022/05/06 Python
webpack介绍使用配置教程详解webpack介绍和使用
2022/06/25 Javascript