原生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);
        }
    }
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值