zoukankan      html  css  js  c++  java
  • 蒙层禁止页面滚动的方案

    蒙层禁止页面滚动的方案

    弹窗是一种常见的交互方式,而蒙层是弹窗必不可少的元素,用于隔断页面与弹窗区块,暂时阻断页面的交互。但是在蒙层出现的时候滚动页面,如果不加处理,蒙层底部的页面会开始滚动,实际上我们是不希望他进行滚动的,因此需要阻止这种行为。当弹出蒙层时禁止蒙层下的页面滚动,也可以称为滚动穿透的问题,文中介绍了一些常用的解决方案。

    实现

    首先需要实现一个蒙层下滚动的效果示例,当我们点击弹窗按钮显示蒙层之后,再滚动鼠标的话能够看到蒙层下的页面依旧是能够滚动的。如果在蒙层的内部进行滚动,当蒙层内滚动条滚动到底部的时候再继续滚动的话,蒙层下的页面也是能够滚动的,这样的交互就比较混乱,文中内容的测试环境是Chrome 96.0.4664.110

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>蒙层禁止页面滚动的方案</title>
        <style type="text/css">
            #mask{
                position: fixed;
                height: 100vh;
                 100vw;
                background: rgba(0, 0, 0, 0.6);
                top: 0;
                left: 0;
                display: flex;
                align-items: center;
                justify-content: center;
            }
            .hide{
                display: none !important;
            }
            .long-content > div{
                height: 300px;
            }
            .mask-content{
                 300px;
                height: 100px;
                overflow-x: auto;
                background: #fff;
            }
            .mask-content > div{
                height: 300px;
            }
        </style>
    </head>
    <body>
        <button id="btn">弹窗</button>
        <div class="long-content">
            <div>long content</div>
            <div>long content</div>
            <div>long content</div>
            <div>long content</div>
            <div>long content</div>
            <div>long content</div>
            <div>long content</div>
        </div>
        <div id="mask" class="hide">
            <div class="mask-content">
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
            </div>
        </div>
    </body>
        <script type="text/javascript">
            (() => {
                const btn = document.getElementById("btn");
                const mask = document.getElementById("mask");
                btn.addEventListener("click", e => {
                    mask.classList.remove("hide");
                })
                mask.addEventListener("click", e => {
                    mask.classList.add("hide");
                })
            })();
        </script>
    </html>
    

    body hidden

    此方案是一种比较常用的方案,即打开蒙层时给body添加overflow: hidden;,在关闭蒙层时就移除这个样式,例如思否的登录弹窗、antdModal对话框就是这样的方式。 这种方案的优点是简单方便,只需添加css样式,没有复杂的逻辑。缺点是在移动端的适配性差一些,部分安卓机型以及safari中,无法阻止底部页面滚动,另外有些机型可能需要给根节点<html>添加overflow: hidden;样式才有效果,此外由于实际上是将页面的内容给裁剪了,所以在设置这个样式的时候滚动条会消失,而移除样式的时候滚动条又会出现,所以在视觉上是会有一定的闪烁现象的,当然也可以定制滚动条的样式,但滚动条样式就是另一个兼容性的问题了,还有同样是因为裁剪。

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>蒙层禁止页面滚动的方案</title>
        <style type="text/css">
            #mask{
                position: fixed;
                height: 100vh;
                 100vw;
                background: rgba(0, 0, 0, 0.6);
                top: 0;
                left: 0;
                display: flex;
                align-items: center;
                justify-content: center;
            }
            .hide{
                display: none !important;
            }
            .long-content > div{
                height: 300px;
            }
            .body-overflow-hidden{
                overflow: hidden;
            }
            .mask-content{
                 300px;
                height: 100px;
                overflow-x: auto;
                background: #fff;
            }
            .mask-content > div{
                height: 300px;
            }
        </style>
    </head>
    <body>
        <button id="btn">弹窗</button>
        <div class="long-content">
            <div>long content</div>
            <div>long content</div>
            <div>long content</div>
            <div>long content</div>
            <div>long content</div>
            <div>long content</div>
            <div>long content</div>
        </div>
        <div id="mask" class="hide">
            <div class="mask-content">
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
            </div>
        </div>
    </body>
        <script type="text/javascript">
            (() => {
                const btn = document.getElementById("btn");
                const mask = document.getElementById("mask");
                const body = document.body;
                btn.addEventListener("click", e => {
                    mask.classList.remove("hide");
                    body.classList.add("body-overflow-hidden");
                })
                mask.addEventListener("click", e => {
                    mask.classList.add("hide");
                    body.classList.remove("body-overflow-hidden");
                })
            })();
        </script>
    </html>
    

    touch preventDefault

    上边的方案对于移动端的效果不是很理想,如果需要在移动端进行处理的话,可以利用移动端的touch事件,来阻止默认行为,当然这是适用于移动端的方式,另外要是把手机通过蓝牙也好转接线也好接上鼠标的话,那就是另一回事了。假如蒙层内容不会有滚动条,那么上述方法是没有问题的,但是假如蒙层内容有滚动条的话,那么它再也无法动弹了。所以如果在蒙层内部有元素需要滚动的话,需要用Js控制其逻辑,但是逻辑控制起来又是比较复杂的,我们可以判断事件的event.target元素,如果touch的目标是弹窗不可滚动区域即背景蒙层就禁掉默认事件,反之就不做控制,之后又出现了问题,需要判断滚动到顶部和滚动到底部的时候禁止滚动,否则触碰到上下两端,弹窗可滚动区域的滚动条到了顶部或者底部,依旧穿透到body,使得body跟随弹窗滚动,这样的话逻辑的复杂程度就比较高了。

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>蒙层禁止页面滚动的方案</title>
        <style type="text/css">
            #mask{
                position: fixed;
                height: 100vh;
                 100vw;
                background: rgba(0, 0, 0, 0.6);
                top: 0;
                left: 0;
                display: flex;
                align-items: center;
                justify-content: center;
            }
            .hide{
                display: none !important;
            }
            .long-content > div{
                height: 300px;
            }
            .mask-content{
                 300px;
                height: 100px;
                overflow-x: auto;
                background: #fff;
            }
            .mask-content > div{
                height: 300px;
            }
        </style>
    </head>
    <body>
        <button id="btn">弹窗</button>
        <div class="long-content">
            <div>long content</div>
            <div>long content</div>
            <div>long content</div>
            <div>long content</div>
            <div>long content</div>
            <div>long content</div>
            <div>long content</div>
        </div>
        <div id="mask" class="hide">
            <div class="mask-content">
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
            </div>
        </div>
    </body>
        <script type="text/javascript">
            (() => {
                const btn = document.getElementById("btn");
                const mask = document.getElementById("mask");
                const body = document.body;
                const scrollerContainer = document.querySelector(".mask-content");
    
                let targetY = 0; // 记录下第一次按下时的`clientY`
                scrollerContainer.addEventListener("touchstart", e => {
                    targetY = Math.floor(e.targetTouches[0].clientY);
                });
                const touchMoveEventHandler = e => {
                    if(!scrollerContainer.contains(e.target)) {
                        e.preventDefault();
                    }
                    let newTargetY = Math.floor(e.targetTouches[0].clientY); //本次移动时鼠标的位置,用于计算
                    let scrollTop = scrollerContainer.scrollTop; // 当前滚动的距离
                    let scrollHeight = scrollerContainer.scrollHeight; // 可滚动区域的高度
                    let containerHeight = scrollerContainer.clientHeight; //可视区域的高度
                    if (scrollTop <= 0 && newTargetY - targetY > 0) { // 到顶
                        console.log("到顶");
                        if(e.cancelable)  e.preventDefault(); // 必须判断`cancelable` 否则容易出现滚动正在进行无法取消的`error`
                    } else if (scrollTop >= scrollHeight - containerHeight && newTargetY - targetY < 0 ) { // 到底
                        console.log("到底");
                        if(e.cancelable) e.preventDefault(); // 必须判断`cancelable` 否则容易出现滚动正在进行无法取消的`error`
                    }
                }
                btn.addEventListener("click", e => {
                    mask.classList.remove("hide");
                    body.addEventListener("touchmove", touchMoveEventHandler, { passive: false });
                })
                mask.addEventListener("click", e => {
                    mask.classList.add("hide");
                    body.removeEventListener("touchmove", touchMoveEventHandler);
                })
            })();
        </script>
    </html>
    

    body fixed

    目前常用的方案就是该方案了,要阻止页面滚动,可以将其固定在视图中即position: fixed,这样它就无法滚动了,当蒙层关闭时再释放,当然还有一些细节要考虑,将页面固定视图后内容会回头最顶端,这里我们需要记录一下用来同步top值,这样就可以得到一个兼容移动端与PC端的较为完善的方案了,当然对于浏览器的api兼容性是使用document.documentElement.scrollTop控制还是window.pageYOffset + window.scrollTo控制就需要另行适配了。在示例中为了演示弹窗时不会导致视图重置到最顶端,将弹窗按钮移动到了下方。

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>蒙层禁止页面滚动的方案</title>
        <style type="text/css">
            #mask{
                position: fixed;
                height: 100vh;
                 100vw;
                background: rgba(0, 0, 0, 0.6);
                top: 0;
                left: 0;
                display: flex;
                align-items: center;
                justify-content: center;
            }
            .hide{
                display: none !important;
            }
            .long-content > div{
                height: 300px;
            }
            .mask-content{
                 300px;
                height: 100px;
                overflow-x: auto;
                background: #fff;
            }
            .mask-content > div{
                height: 300px;
            }
        </style>
    </head>
    <body>
        <div class="long-content">
            <div>long content</div>
            <div>long content</div>
            <div>long content</div>
            <button id="btn">弹窗</button>
            <div>long content</div>
            <div>long content</div>
            <div>long content</div>
            <div>long content</div>
        </div>
        <div id="mask" class="hide">
            <div class="mask-content">
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
                <div>mask-content</div>
            </div>
        </div>
    </body>
        <script type="text/javascript">
            (() => {
                const btn = document.getElementById("btn");
                const mask = document.getElementById("mask");
                const body = document.body;
    
                let documentTop = 0; // 记录按下按钮时的 `top`
    
                btn.addEventListener("click", e => {
                    mask.classList.remove("hide");
                    documentTop = document.scrollingElement.scrollTop;
                    body.style.position = "fixed"
                    body.style.top = -documentTop + "px";
                })
                mask.addEventListener("click", e => {
                    mask.classList.add("hide");
                    body.style.position = "static";
                    body.style.top = "auto";
                    document.scrollingElement.scrollTop = documentTop;
                })
            })();
        </script>
    </html>
    

    每日一题

    https://github.com/WindrunnerMax/EveryDay
    

    参考

    https://zhuanlan.zhihu.com/p/373328247
    https://ant.design/components/modal-cn/
    https://juejin.cn/post/6844903519636422664
    https://segmentfault.com/a/1190000038594173
    https://www.cnblogs.com/padding1015/p/10568070.html
    https://blog.csdn.net/licanty/article/details/86590360
    https://blog.csdn.net/xiaonuanli/article/details/81015131
    
  • 相关阅读:
    [HNOI2002]营业额统计_Treap
    Catch That Cow_bfs
    Knight Moves
    Hie with the Pie
    tp5.1 错误 No input file specified.
    Jmeter通过BeanShell Sampler获取Jmeter的Bin路径,并存入变量供后面的脚本调用
    Jmeter如何把CSV文件的路径设置成一个变量,且变量的值是一个相对路径
    Autotest Weekly Report
    调试Javascript代码(浏览器F12)
    Javascript中escape()、encodeURI()、encodeURIComponent()的区别
  • 原文地址:https://www.cnblogs.com/WindrunnerMax/p/15785653.html
Copyright © 2011-2022 走看看