公共Hooks封装之文字溢出提示useEllipsisPopper

写在前面

对于经常需要开发企业管理后台的前端开发来说,一定会遇到表格内显示字段省略后提示浮窗的需求场景,当然成熟的 UI 框架也已经解决了这一需求场景。但是对于整个项目或系统来说,类似的场景如何更好的复用,以及目前实际使用的 UI 框架可能不符合需求,因此结合实际业务封装了这个Hooks和对应扩展的业务组件。
基于个人项目环境进行封装的Hooks,仅以本文介绍封装Hooks思想心得,故相关代码可能不适用他人

项目环境

Vue3.x + Ant Design Vue3.x + Vite4.x

业务场景分析

图文内容仅供参考,仅提供文章内所需思考对应的图例

在以上图片中,是管理后台系统中常见的表格内容,因使用的是 Ant Design Vue 框架,根据官方的文档中所示: Column 的API ellipsis 超出宽度自动省略,不支持和排序筛选一起使用,,且表格布局将变成 tableLayout="fixed"。 实际使用的代码:

javascript 复制代码
[
  {
      title: '所属角色',
      key: 'role',
      width: 100,
    },
    {
      title: '所在部门',
      key: 'department',
      width: 160,
      ellipsis: true,
    },
]

从上图中则暴露了一个问题,那就是由于 column 作为"配置项"传入表格组件,对于字数可能较长的字段,配置 ellipsis: true 后,无论文本内容是否超出表格列的宽度,都会渲染出 tooltip,从体验方面和性能方面来说,都未必好,渲染了一些"无意义"的DOM。

同样的,在中后台管理系统中,因为业务考虑或 UI 界面设计等等原因,会出现部分显示区域需要显示可能过长的字段内容,而根据技术选型配套的 Ant Design Vue提供了 tooltip 组件依然有上述问题。

Element Plus 如何做的?

作为前端流行的UI框架之一,ElementUI Plus的表格内容,对于上述场景是怎么做的,我们可以从其文档中找到对应的内容~

在上图中,发现 Element Plus 确实对于表格场景,解决了字段根据是否超出再显示 tooltip 的问题。后根据上述配置进行 demo 验证也发现确实可用。 根据官方文档和仓库中的部分源码,发现一个第三方 js 库

Popper.js

TOOLTIP & POPOVER POSITIONING ENGINE

从官方文档及搜索出来各种教程,不难理解,这是一个扩展性较好的 tooltips 提示类 JS 插件,大小仅为 3.5KB 左右,使用与配置也相当简单,基于 popper.js 封装的组件库也有不少,关于这部分的内容,不作为文章重点,且已经有很多介绍其原理和其他相关的优秀内容,在此,不作赘述~

在了解了这个是干嘛的之后,开始着手写项目中需要的Hooks, 使用 popper.js 主要用到 createPopper() 方法,其接受了 3 个参数:reference(需要弹框的按钮Element)、popper(tooltip内容HTMLElement)以及 options

options 内主要用到了 placement(方向) 和 modifiers,Hooks内用到了nameoffset,未考虑其他配置参数,更完善和更复杂的一些封装,可以查看Element PlusTippy.js 等优秀的组件(方法)库。

javascript 复制代码
 const popperInstance = createPopper(parent, tooltipContent, {
    placement: options.placement ?? 'top',
    modifiers: [
      {
        name: 'offset',
        options: {
          offset: [0, 8],
        },
      },
    ],
  });

封装分解:判断逻辑之宽度计算

在查看了Element Plus的文档及源码后,发现其仅在Table组件中有自动省略显示的配置。而对于其他场景,通常我们的做法都是使用tooltip组件,而这种方式并没有考虑实际内容有没有超出。不能做到动态决定是否显示 tooltip。

Hooks 内的做法则是根据 【 子元素的宽度 + 父元素的 padding > 父元素的宽度 ?展示 tooltip : 不展示】

下面内容是关于实现此 Hooks 的一些部分内容拆解

javascript 复制代码
const getPadding = el => {
  const style = window.getComputedStyle(el, null);
  const paddingLeft = Number.parseInt(style.paddingLeft, 10) || 0;
  const paddingRight = Number.parseInt(style.paddingRight, 10) || 0;
  const paddingTop = Number.parseInt(style.paddingTop, 10) || 0;
  const paddingBottom = Number.parseInt(style.paddingBottom, 10) || 0;
  return {
    left: paddingLeft,
    right: paddingRight,
    top: paddingTop,
    bottom: paddingBottom,
  };
};

为什么需要获取父元素 Padding ?这里则是关于 BFC 的一些问题,

判断子元素什么时候需要隐藏并展示 tooltip,根据上图,当 Child container的宽度 + 黄色区域的 padding > Parent container的宽度后,生成 tooltip.

javascript 复制代码
let range = document.createRange();
range.setStart(target, 0);
range.setEnd(target, target.childNodes.length);
const rangeWidth = range.getBoundingClientRect().width;
range.detach();
const { left, right } = getPadding(target);
const horizontalPadding = left + right;

document.createRange()用来创建一个Range对象,包含了startContainerendContainer,在这里我们使用 setStartsetEnd 来创建选择的 DOM 范围,用来拿到 rangeWidth以方便后面的比较计算。 在使用范围后,调用 detach() 方法,以便从创建范围的文档中分离出该范围。

关于这部分内容,以及具体的 CSSOM视图相关的知识,可以查看张鑫旭大佬的文章,文章地址在这:CSSOM视图模式(CSSOM View Module)相关整理

封装分解:创建 tooltipContent

生成 tooltip 的前置条件判断好了,tooltip 的内容要显示什么,本 Hooks 利用的是鼠标移入时获取自定义属性 data-title并将其赋值为innerText,根据 popper.js的文档,创建tooltipContentarrowContent

javascript 复制代码
const renderContent = (target, parent) => {
  const tooltipContent = document.createElement('div');
  const arrowContent = document.createElement('div');
  arrowContent.className = ['ellipsis-tooltip-arrow'].join(' ');
  arrowContent.setAttribute('data-popper-arrow', 'true');
  tooltipContent.innerText = target.dataset.title;
  tooltipContent.setAttribute('role', 'tooltip');
  tooltipContent.appendChild(arrowContent);
  tooltipContent.className = ['ellipsis-tooltip'].join(' ');
  parent.setAttribute('aria-describedby', 'tooltip');
  parent.appendChild(tooltipContent);
  return {
    tooltipContent,
  };
};

同样的,在鼠标移出时,销毁 popperInstance、移除鼠标离开的监听事件。

javascript 复制代码
  popperInstance.destroy();
  parent.removeChild(tooltipContent);
  parent.removeAttribute('aria-describedby');
  target.removeListener('mouseleave', removePopper); 

封装分解:EllipsisPopper.vue 组件

vue 复制代码
<template>
  <div class="ellipsis" :data-title="text" @mouseenter="handleCellMouseEnter">
    <span>{{ text }}</span>
  </div>
</template>

<script setup>
  import { useEllipsisPopper } from '@/hooks';

  defineProps({
    text: {
      type: String,
      required: true,
    },
  });

  const { handleCellMouseEnter } = useEllipsisPopper({ placement: 'auto' });
</script>

因考虑将管理系统中,除表格之外的其他渲染内容,也统一使用动态展示 tooltip,搭配 Hooks 使用,封装 EllipsisPopper 组件。

至此,便理清了Hooks内需要的内容,另外,Hooks内仅考虑了单行文本溢出隐藏展示 tooltip,对于多行文本溢出隐藏后展示 tooltip 的需求并未考虑,相对应的,也没有实现如 Element Plus更复杂的配置,Hooks 本身结合项目实际需求而言,未做更复杂的拓展。

最后,贴一下使用 EllipsisPopper组件和useEllipsisPopper.js后,文章初始的表格变化吧~

Tips 因实际项目需要兼容生态应用(钉钉、飞书)等,需要根据对应开发平台展示部分企业架构相关的字段,文章所示的EllipsisPopper组件仅便于理解,和实际业务组件脱敏处理提取的内容,如果不需要兼容生态应用,则可以直接给父元素(即定款展示区域)自定义属性data-title,并加上 @mouseenter="handleCellMouseEnter"

最后,贴一下完整代码~

useEllipsisPopper.js完整代码

javascript 复制代码
import { createPopper } from '@popperjs/core';

const getPadding = el => {
  const style = window.getComputedStyle(el, null);
  const paddingLeft = Number.parseInt(style.paddingLeft, 10) || 0;
  const paddingRight = Number.parseInt(style.paddingRight, 10) || 0;
  const paddingTop = Number.parseInt(style.paddingTop, 10) || 0;
  const paddingBottom = Number.parseInt(style.paddingBottom, 10) || 0;
  return {
    left: paddingLeft,
    right: paddingRight,
    top: paddingTop,
    bottom: paddingBottom,
  };
};

const renderContent = (target, parent) => {
  const tooltipContent = document.createElement('div');
  const arrowContent = document.createElement('div');
  arrowContent.className = ['ellipsis-tooltip-arrow'].join(' ');
  arrowContent.setAttribute('data-popper-arrow', 'true');
  tooltipContent.innerText = target.dataset.title;
  tooltipContent.setAttribute('role', 'tooltip');
  tooltipContent.appendChild(arrowContent);
  tooltipContent.className = ['ellipsis-tooltip'].join(' ');
  parent.setAttribute('aria-describedby', 'tooltip');
  parent.appendChild(tooltipContent);
  return {
    tooltipContent,
  };
};

export function useEllipsisPopper(options = {}) {
  const handleCellMouseEnter = event => {
    const target = event.target;
    const parent = target.parentNode;
    let range = document.createRange();
    range.setStart(target, 0);
    range.setEnd(target, target.childNodes.length);
    const rangeWidth = range.getBoundingClientRect().width;
    range.detach();
    const { left, right } = getPadding(target);
    const horizontalPadding = left + right;
    if (Math.floor(rangeWidth + horizontalPadding) > target.clientWidth) {
      const { tooltipContent } = renderContent(target, parent);
      const popperInstance = createPopper(parent, tooltipContent, {
        placement: options.placement ?? 'top',
        modifiers: [
          {
            name: 'offset',
            options: {
              offset: [0, 8],
            },
          },
        ],
      });

      const removePopper = () => {
        popperInstance.destroy();
        parent.removeChild(tooltipContent);
        parent.removeAttribute('aria-describedby');
        target.removeListener('mouseleave', removePopper);
      };

      target.addEventListener('mouseleave', removePopper);
    }
  };

  return {
    handleCellMouseEnter,
  };
}

需要额外补充的样式至项目内

css 复制代码
.ellipsis-tooltip {
  z-index: 10;
  display: inline-block;
  background: #333333;
  color: #ffffff;
  padding: 5px 10px;
  font-size: 13px;
  border-radius: 4px;
}

.ellipsis-tooltip-arrow,
.ellipsis-tooltip-arrow::before {
  position: absolute;
  width: 6px;
  height: 6px;
  background: inherit;
}

.ellipsis-tooltip-arrow {
  visibility: hidden;
}

.ellipsis-tooltip-arrow::before {
  visibility: visible;
  content: '';
  transform: rotate(45deg);
}

.ellipsis-tooltip[data-popper-placement^='top'] > .ellipsis-tooltip-arrow {
  bottom: -3px;
}

.ellipsis-tooltip[data-popper-placement^='bottom'] > .ellipsis-tooltip-arrow {
  top: -3px;
}

.ellipsis-tooltip[data-popper-placement^='left'] > .ellipsis-tooltip-arrow {
  right: -3px;
}

.ellipsis-tooltip[data-popper-placement^='right'] > .ellipsis-tooltip-arrow {
  left: -3px;
} 

参考链接

写在最后

  • 文章内容为个人基于实际项目并脱敏处理的原创内容,如需要转载请备注原文链接~
  • 如果看完文章对于你有帮助,就来一键三连吧~更欢迎您在评论区进行指点
相关推荐
爱上妖精的尾巴8 小时前
8-5 WPS JS宏 match、search、replace、split支持正则表达式的字符串函数
开发语言·前端·javascript·wps·jsa
小温冲冲8 小时前
通俗且全面精讲单例设计模式
开发语言·javascript·设计模式
意法半导体STM329 小时前
【官方原创】FDCAN数据段波特率增加后发送失败的问题分析 LAT1617
javascript·网络·stm32·单片机·嵌入式硬件·安全
为什么不问问神奇的海螺呢丶9 小时前
n9e categraf redis监控配置
前端·redis·bootstrap
云飞云共享云桌面9 小时前
推荐一些适合10个SolidWorks设计共享算力的服务器硬件配置
运维·服务器·前端·数据库·人工智能
Liu.7749 小时前
vue开发h5项目
vue.js
咔咔一顿操作9 小时前
轻量无依赖!autoviwe 页面自适应组件实战:从安装到源码深度解析
javascript·arcgis·npm·css3·html5
刘联其9 小时前
.net也可以用Electron开发跨平台的桌面程序了
前端·javascript·electron
韩曙亮9 小时前
【jQuery】jQuery 选择器 ④ ( jQuery 筛选方法 | 方法分类场景 - 向下找后代、向上找祖先、同级找兄弟、范围限定查找 )
前端·javascript·jquery·jquery筛选方法
前端 贾公子9 小时前
Node.js 如何处理 ES6 模块
前端·node.js·es6