通过 JavaScript 脚本解决移动端弹出层滚动穿透的问题

前端开发中经常会遇到一个麻烦的问题:在通过fixed或者absolute定位的弹出层上进行滚动操作的时候,滚动事件会穿透到底部导致body跟着滚动,及其影响交互体验。常规做法是在弹出层时候禁止body的滚动,将其设置为overflow: hidden,但是会导致滚动高度丢失从而造成视觉上一次弹动,所以我决定从弹出层本身考虑这个问题。

<body>
  Body content.
  <div id="popup" style="position: fixed; top: 0; right: 0; left: 0; bottom: 0">
    Popup layer.
  </div>
  <script type="text/javascript">
  const canScrollEl = ($el, up) => {
    const overflow = up
      ? Math.abs($el.scrollTop) > Number.EPSILON
      : $el.scrollHeight - $el.scrollTop - $el.clientHeight > Number.EPSILON;
    if (!overflow) {
      return false;
    }
    const styles = getComputedStyle($el);
    if (styles['overflow-y'] !== 'auto' && styles['overflow-y'] !== 'scroll') {
      return false;
    }
    return true;
  };

  const canScrollEls = ($child, $root, up) => {
    let $el = $child;
    while ($el) {
      if (canScrollEl($el, up)) {
        return true;
      }
      if ($el === $root) {
        break;
      }
      $el = $el.parentNode;
    }
    return false;
  };

  const preventEvent = (e) => {
    e.preventDefault();
    e.stopPropagation();
    e.returnValue = false;
    return false;
  };

  const eventData = {
    touchesPos: {},
    moving: false,
    canScroll: false,
  };
  const el = document.getElementById('popup');

  el.addEventListener('mousewheel', (e) => {
    if (canScrollEls(e.target, el, e.wheelDelta > 0)) {
      return void 0;
    }
    return preventEvent(e);
  }, { capture: true, passive: false, once: false });

  el.addEventListener('touchstart', (e) => {
    // record touch start pos
    Object.keys(e.changedTouches).forEach((i) => {
      const touch = e.changedTouches[i];
      eventData.touchesPos[touch.identifier] = {
        startY: touch.clientY,
        currentY: touch.clientY,
      };
    });
    eventData.moving = false;
    eventData.canScroll = false;
  }, { capture: true, passive: false, once: false });

  el.addEventListener('touchmove', (e) => {
    // update current touch pos and calc touches sum delta distance
    let touchDeltaY = 0;
    Object.keys(e.changedTouches).forEach((i) => {
      const touch = e.changedTouches[i];
      const cache = eventData.touchesPos[touch.identifier];
      if (cache) {
        touchDeltaY += touch.clientY - cache.currentY;
        cache.currentY = touch.clientY;
      }
    });
    const canScroll = canScrollEls(e.target, el, touchDeltaY > 0);
    if (!eventData.moving) { // if first move cannot scroll this layer, all move after will not scroll this layer
      eventData.moving = true;
      eventData.canScroll = canScroll;
    }
    if (canScroll && eventData.canScroll) {
      return void 0;
    }
    return preventEvent(e);
  }, { capture: true, passive: false, once: false });

  el.addEventListener('touchend', (e) => {
    if (e && e.touches && e.touches.length !== 0) {
      return;
    }
    eventData.touchesPos = {};
  }, { capture: true, passive: false, once: false });
  </script>
</body>

如果直接禁止touchmove事件,那么子元素有滚动区域时将也无法滚动,所以我们用上面一段代码来判断,动态判断是否要禁止事件。

如果你是Vue开发者,直接使用封装好的包即可:https://www.npmjs.com/package/vue-prevent-overscroll.js

所有原创文章采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。
您可以自由的转载和修改,但请务必注明文章来源并且不可用于商业目的。
本站部分内容收集于互联网,如果有侵权内容、不妥之处,请联系我们删除。敬请谅解!

  Previous post Install pydio cells with docker on CentOS 7.
Next post   已到最新一篇

添加新评论

  关于博主

  近期评论

  •  红小胖: 还是不可以 是为什么?
  •  Gill: 这个好玩。。。用了你的统计插件,我也发现有人扫,很不爽
  •  whc2001: 作者你好, 在新机器上运行似乎有bug, 会抛出异常"给定关键字不在字典中"...
  •  libcrypto: bomb 的原理是压缩后 1MB 解压后 1GB 的文件。因为在如果返回 gzip 类型的文件...
  •  天价萌: 额……现在爬站不都是异步的么……
  •  司空白: 您做的chorme的汇率插件最近一段时间连接不上了,可以抽空更新一下吗? 感谢大神
  •  风星璇: 说错了是现在还能不能监控别人的
  •  风星璇: 您好,请问剑三插件能支持监控自己的技能CD倒计时吗?
  •  小可可: 出酬劳,请帮忙给解决个问题!请联系我
  •  xiao酱沫: 重启机器,按住command+r进入恢复模式,打开终端输入csrutil disable 。然...

昆仑玄境山外山,乾坤阴阳有洞天。只问真君何处有,不向江湖寻剑仙。

长空令在,浩气长存。

玄剑化生雷霆势,镇我山河几度春。

万物皆虚幻,大道本无形。跳出三界外,不在五行中。

世人皆沉浸在纸醉金迷之中,往往要等到大祸临头或者一无所有才能幡然悔悟。凡事种种,皆是欲望使然。无欲则无悲无喜,恬淡自然。

以手中三尺长剑,镇四方八秒山河。

方才我喝了杯茶。