原生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实现多级联动下拉菜单的方法
Feb 06 Javascript
JavaScript 表单处理实现代码
Apr 13 Javascript
详解JavaScript对象类型
Jun 16 Javascript
Javascript数组循环遍历之forEach详解
Nov 07 Javascript
关于微信上网页图片点击全屏放大效果
Dec 19 Javascript
详解如何使用Vue2做服务端渲染
Mar 29 Javascript
在Vue中使用echarts的实例代码(3种图)
Jul 10 Javascript
Vue 组件间的样式冲突污染
Aug 31 Javascript
小程序tab页无法传递参数的方法
Aug 03 Javascript
JavaScript类的继承多种实现方法
May 30 Javascript
浅谈vue中document.getElementById()拿到的是原值的问题
Jul 26 Javascript
vue3.0实现点击切换验证码(组件)及校验
Nov 18 Vue.js
原生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
easyui的tabs update正确用法分享
2014/03/21 PHP
thinkphp连贯操作实例分析
2014/11/22 PHP
php创建、获取cookie及基础要点分析
2015/01/26 PHP
laravel中命名路由的使用方法
2017/02/24 PHP
JavaScript性能陷阱小结(附实例说明)
2010/12/28 Javascript
用原生JavaScript实现jQuery的$.getJSON的解决方法
2013/05/03 Javascript
javascript实现window.print()去除页眉页脚
2014/12/30 Javascript
JavaScript检测上传文件大小的方法
2015/07/22 Javascript
Jquery中使用show()与hide()方法动画显示和隐藏图片
2015/10/08 Javascript
JS中递归函数
2016/06/17 Javascript
jQuery实现的无限级下拉菜单功能示例
2016/09/12 Javascript
Bootstrap免费字体和图标网站(值得收藏)
2017/03/16 Javascript
基于Cookie常用操作以及属性介绍
2017/09/07 Javascript
使用原生js封装的ajax实例(兼容jsonp)
2017/10/12 Javascript
手写Node静态资源服务器的实现方法
2018/03/20 Javascript
d3.js实现自定义多y轴折线图的示例代码
2018/05/30 Javascript
理顺8个版本vue的区别(小结)
2018/09/17 Javascript
对angularJs中自定义指令replace的属性详解
2018/10/09 Javascript
微信小程序拍照和摄像功能实现方法示例
2019/02/01 Javascript
jQuery ajax仿Google自动提示SearchSuggess功能示例
2019/03/28 jQuery
javascript操作元素的常见方法小结
2019/11/13 Javascript
[02:31]2014DOTA2国际邀请赛2009专访:干爹表现出乎意料 看好DK杀回决赛
2014/07/20 DOTA
python使用7z解压apk包的方法
2015/04/18 Python
简单谈谈python中的多进程
2016/11/06 Python
pyenv与virtualenv安装实现python多版本多项目管理
2019/08/17 Python
Python实现实时数据采集新型冠状病毒数据实例
2020/02/04 Python
查看已安装tensorflow版本的方法示例
2020/04/19 Python
HTML5 input新增type属性color颜色拾取器的实例代码
2018/08/27 HTML / CSS
应用艺术毕业生的自我评价
2013/12/04 职场文书
护理专科自荐书范文
2014/02/18 职场文书
《小动物过冬》教学反思
2014/04/17 职场文书
小学教师师德承诺书
2014/05/23 职场文书
培训科主任岗位职责
2014/08/08 职场文书
民主生活会整改措施(党员)
2014/09/18 职场文书
三傻大闹宝莱坞观后感
2015/06/03 职场文书
2019年冬至:天冷暖人心的问候祝福语大全
2019/12/20 职场文书