1. 程式人生 > 其它 >蒙層禁止頁面滾動的方案

蒙層禁止頁面滾動的方案

蒙層禁止頁面滾動的方案

彈窗是一種常見的互動方式,而蒙層是彈窗必不可少的元素,用於隔斷頁面與彈窗區塊,暫時阻斷頁面的互動。但是在蒙層出現的時候滾動頁面,如果不加處理,蒙層底部的頁面會開始滾動,實際上我們是不希望他進行滾動的,因此需要阻止這種行為。當彈出蒙層時禁止蒙層下的頁面滾動,也可以稱為滾動穿透的問題,文中介紹了一些常用的解決方案。

實現

首先需要實現一個蒙層下滾動的效果示例,當我們點選彈窗按鈕顯示蒙層之後,再滾動滑鼠的話能夠看到蒙層下的頁面依舊是能夠滾動的。如果在蒙層的內部進行滾動,當蒙層內滾動條滾動到底部的時候再繼續滾動的話,蒙層下的頁面也是能夠滾動的,這樣的互動就比較混亂,文中內容的測試環境是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;
            width: 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{
            width: 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;
            width: 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{
            width: 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;
            width: 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{
            width: 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;
            width: 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{
            width: 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