【JS Fix】zoom 缩放导致 getBoundingClientRect 属性不准确的解决方案 (可解决 ElementUI, AntDesign 等悬浮组件偏移问题)

分析问题

  • 现代浏览器中标准化了 CSS zoom 属性(如 Chrome 128 以上版本更新),不再与先前非标准化时的处理规范一致,getBoundingClientRect, getClientRects 获取到的 ClientRect 实例不再计算所有父元素中的缩放层级,导致获取到的属性不准确。
    Chrome 128 以上版本 CSS zoom 属性的变化
  • 因 ElementUI, AntDesign 等悬浮组件均使用 ClientRect 计算定位,且组件继承的父元素 CSS zoom 属性存在值不为 1 的变化,则导致悬浮组件的定位样式出现偏移。
    Chrome zoom 变化对比图

解决方案

  • 解决核心问题:使获取到的 ClientRect 实例计算所有父元素中的缩放层级。
  • 获取元素的实际缩放层级,需计算所有继承的 zoom 比例,不能仅计算 bodyzoom 比例,以免其父元素之一存在 zoom != 1 时导致比例畸变。
  • 以下为实测可用的通用解决方案(兼容 Chrome, Firefox, Edge 等主流浏览器):
    /** [Hotfix] 修复 ClientRect 受 zoom 影响导致属性不精确的问题 */
    if (CSS.supports('zoom', '1')) {
      const isUseFix = function () {
        let el = document.createElement('div')
        el.style.cssText = 'position: fixed; width: 100px; top: 0; left: 0; z-index: -1;'
        document.body.appendChild(el)
        let a = el.getBoundingClientRect().width
        el.style.zoom = 2
        let b = el.getBoundingClientRect().width
        document.body.removeChild(el)
        return a !== b
      }
      if (isUseFix()) {
        const useDOMRect = typeof DOMRect === 'function'
        const useDOMRectList = typeof DOMRectList === 'function'
        const _getBoundingClientRect = Element.prototype.getBoundingClientRect
        const _getClientRects = Element.prototype.getClientRects
        const getRectItem = function (i) { return this[i] }
        const getRealZoom = function (el) {
          let zoom = 1
          while (el) zoom /= +getComputedStyle(el).zoom || 1, el = el.parentElement
          return zoom
        }
        const newRealRect = function ({ width, height, left, top, x = left, y = top }, zoom) {
          return useDOMRect ? new DOMRect(x * zoom, y * zoom, width * zoom, height * zoom) : {
            x: x * zoom, y: y * zoom, width: width * zoom, height: height * zoom,
            top: y * zoom, left: x * zoom, right: x * zoom + width * zoom, bottom: y * zoom + height * zoom
          }
        }
        Element.prototype.getBoundingClientRect = function () {
          let rect = _getBoundingClientRect.call(this), zoom = getRealZoom(this)
          return zoom != 1 ? newRealRect(rect, zoom) : rect
        }
        Element.prototype.getClientRects = function () {
          let list = _getClientRects.call(this), zoom = getRealZoom(this)
          if (zoom != 1) {
            list = [...list].map(rect => newRealRect(rect, zoom))
            Object.defineProperty(list, 'item', { value: getRectItem })
            useDOMRectList && Object.setPrototypeOf(list, DOMRectList.prototype)
          }
          return list
        }
      }
    }
    
  • 使用 API 方式调用:
    /**
     *  获取实际的 ClientRect 属性
     *  @param  {HTMLElement} el  DOM元素
     *  @return {DOMRect|Object}
     */
    function getRealClientRect(el) {
      const rect = el.getBoundingClientRect()
      const isUseFix = CSS.supports('zoom', '1') && (function () {
        let checkEl = document.createElement('div')
        checkEl.style.cssText = 'position: fixed; width: 100px; top: 0; left: 0; z-index: -1;'
        document.body.appendChild(checkEl)
        let a = checkEl.getBoundingClientRect().width
        checkEl.style.zoom = 2
        let b = checkEl.getBoundingClientRect().width
        document.body.removeChild(checkEl)
        return a !== b
      })()
      if (isUseFix) {
        let target = el, zoom = 1
        while (target) zoom /= +getComputedStyle(target).zoom || 1, target = target.parentElement
        if (zoom != 1) {
          let { width, height, left, top, x = left, y = top } = rect
          return typeof DOMRect === 'function' ? new DOMRect(x * zoom, y * zoom, width * zoom, height * zoom) : {
            x: x * zoom, y: y * zoom, width: width * zoom, height: height * zoom,
            top: y * zoom, left: x * zoom, right: x * zoom + width * zoom, bottom: y * zoom + height * zoom
          }
        }
      }
      return rect
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值