监测DOM元素尺寸大小变化


theme: cyanosis

第一反应是什么?

window.addEventListener("resize", handler, useCapture)

简单粗暴,但缺点也明显:文档视图调整大小时会触发 resize 事件,针对的 仅是window,而普通的DOM元素没有 onresize 事件的。

所以严格来说,这个第一反应不算正确

其他想法:window.matchMedia

在 JavaScript 中,你可以通过 window.matchMedia()方法检测 CSS 中定义的相同的 CSS 媒体查询字符串。例如,在 CSS 中,如果你有以下的 CSS 媒体查询。

/* #### 当查看区域的宽度为 765px 或更小时应用的 CSS #### */
@media screen and (max-width: 765px){
  /* CSS定义在这里 */
}

要在 JavaScript 中检测相同的媒体查询,你要做以下工作。

var mql = window.matchMedia("screen and (max-width: 765px)")

window.matchMedia 的应用与封装 – 知乎 (zhihu.com)

可展开/收缩搜索条件且支持自适应功能 – 掘金 (juejin.cn)

主角:ResizeObserver

image.png

用法

var ro = new ResizeObserver( entries => {
  for (let entry of entries) {
    const cr = entry.contentRect;
    console.log('Element:', entry.target);
    console.log(`Element size: ${cr.width}px x ${cr.height}px`);
    console.log(`Element padding: ${cr.top}px ; ${cr.left}px`);
  }
});

// 观察一个或多个元素
ro.observe(eleZxx);

vue包

@juggle/resize-observer – npm (npmjs.com)

ResizeObserver实际可以用来做什么

实现类似效果的菜单(Overflow Menu)

show-12.gif

原理

  1. 获取菜单容器宽度
  2. 获取每个菜单项宽度
  3. 当菜单容器宽度改变时,判断菜单项总宽度是否大于菜单容器宽度,如果是,则将菜单项从尾到头,依次隐藏,直至可显示的菜单项总宽度小于等于菜单容器宽度
  4. 当菜单容器宽度改变时,判断菜单项总宽度是否小于菜单容器宽度,如果是,则将菜单项从头到为,依次显示,直至可显示的菜单项达到菜单容器的极限
  5. 当菜单容器宽度改变时,判断菜单项总宽度是否等于菜单容器宽度,如果是,则将菜单项全部显示

vue简单实现

<!--

@author: pan
@createDate: 2022-11-19 15:21
-->
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, toRefs, watch } from 'vue'
import { ResizeObserver } from '@juggle/resize-observer'

export interface MenuItem {
  key: string
  name: string
}
interface MenuItemInner extends MenuItem {
  //是否显示
  responseShow: boolean
  // 宽度
  width: number
}
const props = withDefaults(
  defineProps<{
    menuArr?: MenuItem[]
  }>(),
  { menuArr: () => [] as MenuItem[] }
)
const { menuArr: tmpArr } = toRefs(props)
const menuArr = ref<MenuItemInner[]>([])
watch(
  tmpArr,
  newVal => {
    menuArr.value = newVal.map(item => ({
      ...item,
      responseShow: true,
      width: 0,
    }))
  },
  { immediate: true }
)
const menuContainerRef = ref<HTMLUListElement>()
onMounted(() => {
  // 获取每个li的长度
  const menuContainerEl: HTMLUListElement =
    menuContainerRef.value as HTMLUListElement
  menuContainerEl.querySelectorAll('li').forEach((item, idx) => {
    const width = item.offsetWidth
    menuArr.value[idx].width = width
  })
})
interface MenuResponseShowData {
  // 可见li总宽度
  menuItemWidth: number
  // 可见li集合
  responseShowDataArr: MenuItemInner[]
}
/**
 * 菜单容器大小改变的回调函数
 *
 * @param menuContainerEl 菜单容器
 */
function menuContainerSizeChange(menuContainerEl: HTMLUListElement): void {
  // 获取菜单容器宽度
  const menuContainerWidth = menuContainerEl.offsetWidth
  // 计算可见li的总宽度,以及可见li集合
  const { menuItemWidth, responseShowDataArr } =
    menuArr.value.reduce<MenuResponseShowData>(
      (prevVal, curItem) => {
        if (curItem.responseShow) {
          prevVal.menuItemWidth += curItem.width
          prevVal.responseShowDataArr.push(curItem)
        }
        return prevVal
      },
      { menuItemWidth: 0, responseShowDataArr: [] }
    )
  if (menuItemWidth > menuContainerWidth) {
    // li的总宽度超过菜单容器宽度
    let tmpMenuItemWidth = menuItemWidth
    for (let i = responseShowDataArr.length; i >= 0; i--) {
      tmpMenuItemWidth -= responseShowDataArr[i - 1].width
      // 从后向前,将可见菜单项设置为不可见,直至可见菜单项的总长度小于等于菜单容器长度
      responseShowDataArr[i - 1].responseShow = false
      if (tmpMenuItemWidth <= menuContainerWidth) {
        break
      }
    }
  } else if (menuItemWidth < menuContainerWidth) {
    // li的总宽度小于菜单容器宽度
    let tmpMenuItemWidth = 0
    const tmpMenuArr = menuArr.value
    for (let i = 0; i < tmpMenuArr.length; i++) {
      tmpMenuItemWidth += tmpMenuArr[i].width
      // 从前向后,将菜单设置为可见,直至可见菜单项的总长度等于菜单容器长度
      tmpMenuArr[i].responseShow = true
      if (tmpMenuItemWidth === menuContainerWidth) {
        break
      } else if (tmpMenuItemWidth > menuContainerWidth) {
        tmpMenuArr[i].responseShow = false
        break
      }
    }
  } else {
    // li的总宽度等于菜单容器宽度
    const tmpMenuArr = menuArr.value
    for (let i = 0; i < tmpMenuArr.length; i++) {
      tmpMenuArr[i].responseShow = true
    }
  }
}

const ro = new ResizeObserver(entries => {
  for (let entry of entries) {
    switch (entry.target) {
      case menuContainerRef.value: {
        menuContainerSizeChange(menuContainerRef.value as HTMLUListElement)
        break
      }
    }
  }
})
onMounted(() => {
  const menuContainerEl = menuContainerRef.value as HTMLUListElement
  ro.observe(menuContainerEl)
})
onBeforeUnmount(() => {
  ro.disconnect()
})
</script>

<template>
  <div>
    <ul ref="menuContainerRef" class="menuContainer">
      <template v-for="menuItem of menuArr" :key="menuItem.key">
        <li v-if="menuItem.responseShow !== false">
          {{ menuItem.name }}
        </li>
      </template>
    </ul>
  </div>
</template>

<style lang="scss" scoped>
ul,
li {
  list-style-type: none;
  margin: 0;
  padding: 0;
}
.menuContainer {
  display: flex;
  li {
    padding: 10px 20px;
    border-right: 1px solid #333;
    &:last-child {
      border-right-width: 0;
    }
  }
}
</style>

参考文章

监测DOM元素尺寸大小变化|Vue – 掘金 (juejin.cn)

检测DOM尺寸变化JS API ResizeObserver简介 « 张鑫旭-鑫空间-鑫生活 (zhangxinxu.com)

Navbar with Overflow Menu (codepen.io)

Ant Design 4.0 的一些杂事儿 – Overflow 篇 – 知乎 (zhihu.com)

如何使用JS检测用户是否缩放了页面? « 张鑫旭-鑫空间-鑫生活 (zhangxinxu.com)

© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容