移动端弹出层后禁止背景底层 body滚动

触发页面上某个元素或者达到某个条件时,页面弹出模态框的场景应该是很常见的了,特别是在屏幕较小的移动端,例如下面这种:

这里写图片描述

对于这个效果,之前一直都没怎么在意探究过,因为觉得应该没什么好弄的,直到,我接到了一个包含此效果的需求之后,我才知道什么叫太样太森破,有时拿衣服。


body: overflow:hidden

第一次尝试这个效果的时候,我稍稍思考了一下,觉得给 body加个 overflow:hidden;height: 100vh;的样式应该就可以了,所以就这么写了。

DOM如下:

// HTML
<div class="box">
   <div class="box1">
   </div>
 </div>

当弹出层出现时,给 body设置样式:

document.body.style.height = '100%'
document.body.style.overflow = 'hidden'

写完后顺便在我的几个浏览器上试了一下,桌面浏览器模拟移动端的那种,效果杠杠滴,和我预想的一样,没毛病。

but,哪有这么简单的事情?

桌面浏览器是 ok了,但是这个项目的主要场景是 移动端,所以要通过真正的移动端浏览器才行,桌面浏览器模拟的可不算数,结果移动端浏览器中,除了 移动chrome之外,全跪了,给 body加上那两个样式和没加的效果是一样的,背景层改怎么滚动还怎么滚动。


body+html: overflow:hidden

百思不得其解之下,只好跑到网上找了一圈,结果真让我又找到了一个答案,说是仅仅给 body设置 overflow是不行的,还必须同时给 html节点也加上这个样式才行,于是就试了一下。

document.documentElement.style.height = '100%'
document.documentElement.style.overflow = 'hidden'
document.body.style.height = '100%'
document.body.style.overflow = 'hidden'

桌面浏览器模拟移动端测试通过,之前跪了的移动浏览器现在也都 ok了,给这两个元素加上上述样式后,弹出层背景 body确定是不会滚动了。

but,又出现了另外一个问题,当将页面往下滚动一段距离,也就是说 document.body.scrollTop 大于 0时,再显示弹出层,增加上述四行代码时,页面自动滚到了最顶部,也就是说浏览器像是自动执行了这一行代码 document.body.scrollTop=0

仔细想想也是,之前页面是超出一个屏幕高度的,所以可以滚动,但是现在你把页面高度设为一个屏幕高度 100%,并且 overflow:hidden,那么根据 overflow:hidden的特性,浏览器肯定是要从页面的头部开始截取一个屏幕的高度,剩下的再 hidden

如果弹出层时,背景是完全看不到的,一片漆黑,也就是类似 rgba(0,0,0,1),而不是半透明 rgba(0,0,0,.6),那么实际无伤大雅,也就是一行代码的事情。

在弹出层弹出之前,先保存此事页面的 scrollTop,然后在弹出层关闭的时候,再将页面的 scrollTop设定到之前保存的那个位置,所以这样最起码看起来背景是没变的,就像下面这样:

var box1 = $('.box1')
var body= document.body
var scrolltop
body.addEventListener('click', function() {
  if (box1.className.indexOf('hidden') !== -1) {
    // 保存页面滚动到的位置
    scrolltop = document.body.scrollTop

    body.style.height = '100%'
    body.style.overflow = 'hidden'
    document.documentElement.style.height = '100%'
    document.documentElement.style.overflow = 'hidden'
    // 显示弹出层
    box1.className ='box1 show'
  } else {
    // 隐藏弹出层
    box1.className ='box1 hidden'
    body.style.height = 'auto'
    body.style.overflow = 'visible'
    document.documentElement.style.height = 'auto'
    document.documentElement.style.overflow = 'visible'
    // 恢复页面滚动到的位置
    document.body.scrollTop = scrolltop
  }
})

如果背景层是可见的呢,只要用户不瞎,肯定能看到页面发生跳动了啊。


JS控制

  • 区别对待

看来只通过 css来完成这个效果是有些难度了,于是将主意打到了 js上,如下:

box1.addEventListener('touchmove', function(e){
    e.preventDefault()
})

直接禁止弹出层的掉滚动事件,因为弹出层是满屏覆盖在 页面上的,而且这个事件也没有 点透,所以确实是达到了禁止背景 页面滚动的效果。

but,背景元素的滚动是禁止掉了,但这种禁止几乎是把页面上所有元素的滚动事件都禁止掉了,如果在弹出层元素 box1中存在可以滚动的元素,那么同样也会被禁止滚动,这可不是我们想要的,所以必须要把弹出层内的元素排除在外才行。

弹出层内可滚动区域包括可滚动的顶级元素,以及此元素下所有的子元素,所以只要判断当前正在滚动的区域是此区域内的元素,则允许滚动,所以这里需要判断当前 touch的元素是不是弹出层内可以滚动的元素,以及是不是其子元素,判断是否为其子元素只需要一个循环递归即可,例如以下代码:

function getRecursiveEle(ele, parentClassName) {
  if (ele.className.indexOf(parentClassName) !== -1) {
    return ele
  } else {
    if (ele.nodeName.toLowerCase() === 'body') {
      return null
    }
    ele = ele.parentNode
    return getRecursiveEle(ele, parentClassName)
  }
}

调用此函数,传入 e.target以及弹出层可以滚动的元素类名即可,返 回 true,则表明是可以滚动的元素。

but,虽然你直接禁止了弹出层可滚动元素其外的元素滚动,然而同时又允许弹出层内可滚动元素滚动,那么当将滚动元素滚动到头的时候,背景还是会滚动。

所以,还需要加一个判断,当滚动元素滚动到头或者尾部的时候,再禁止所有元素滚动,这里的滚动到头包括两种情况,到头和到尾。

// 滚动到头的情况,其中touchstartY 为开始滚动时接触点的 `pageY`
if(modal.scrollTop <= 0) {
    e.targetTouches[0].pageY > touchstartY && e.preventDefault()
}
// 滚动到尾的情况,其中touchstartY 为开始滚动时接触点的 `pageY`,itemH为可滚动元素框内部的子元素总高度,+2是因为边界问题
(itemH - modal.offsetHeight < modal.scrollTop + 2) && (e.targetTouches[0].pageY < touchstartY) && e.preventDefault()

这样的话,问题大体上解决了,但还有点小问题,在有的浏览器上,当可滚动元素滚动到头的时候,背景依旧还是会稍微滚动一点距离,不太完美。

  • 另想他法

依旧让覆盖整个屏幕的弹出层禁止滚动,弹出层内部可滚动元素的滚动通过 js来控制,例如使用 translate控制上下滚动距离。

// touchstartY 为touchstart事件发生时的 e.targetTouches[0].pageY
var translateEndY = 0, translateEndYTemp  = 0
box1.addEventListener('touchmove', function(e) {
    // 禁止默认滚动
    e.preventDefault()
    translateEndYTemp = e.targetTouches[0].pageY-touchstartY + translateEndY
    // 通过改变 translate来滚动元素
    $('.item').style.transform = 'translate(0, '+translateEndYTemp+'px)'
})

// 缓存下每次滚动过的距离
box1.addEventListener('touchend', function(e) {
  translateEndY = translateEndYTemp
})

嗯,这样就差不多了,不过因为滚动时通过 translate实现的,所以滚动元素是不受父元素约束的,也就是说滚动元素会滚过界,这个很好解决,在 touchend的时候,判断一下有没有过界,如果过界了反弹回来就行。

猜你喜欢

转载自blog.csdn.net/deeplies/article/details/78206575