element-plus中el-tree实现自适应横向滚动条

一、实现思路

动态设置高度和宽度,高度很容易,就是el-tree本身的高度,困难点是如何找到应该设置的宽度,我的思路是直接强行一级节点及其展开节点中最宽的一个元素,取这个元素的宽度,来动态设置整个容器的宽度。

二、实现过程

1.监视元素el-tree尺寸变化

首先是如何监视元素的宽高度改变,这里我们实现一个监视元素宽高改变的hooks 具体Api文档:developer.mozilla.org/zh-CN/docs/...

js 复制代码
import { onMounted, onUnmounted, ref } from "vue";
import * as _ from "lodash";
const { isString, throttle } = _;

/**
* ResizeObserver 构造函数创建一个新的 ResizeObserver 对象,它可以用于监听 Element 内容盒或边框盒或者 SVGElement 边界尺寸的大小。
* @param el
* @param callback
* @param config
*/
const useResizeObserver = (
 el: HTMLElement | string,
 callback: (mutationList: ResizeObserverEntry[]) => void = () => {},
 config: {
   throttleTime: number;
 } = { throttleTime: 0 }
) => {
 const observer = ref<ResizeObserver>();
 const observerCallback = throttle(callback, config.throttleTime);
 /**
  *
  * @param el {HTMLElement}  需要观察的元素
  */
 const creatObserver = function (el: HTMLElement) {
   if (!el) return;
   // 选择需要观察变动的节点
   const targetNode = el;

   // 创建一个观察器实例并传入回调函数
   const observer = new ResizeObserver(observerCallback);

   // 以上述配置开始观察目标节点
   observer.observe(targetNode);
   return observer;
 };
 onMounted(() => {
   const targetObserverEl = isString(el)
     ? (document.querySelector(el) as HTMLElement)
     : el;
   if (targetObserverEl) {
     observer.value = creatObserver(targetObserverEl);
   }
 });

 onUnmounted(() => {
   observer.value?.disconnect?.();
 });
 return {
   observer,
 };
};
export default useResizeObserver;

1.开始封装

文件目录结构

ScrollTree.ts

js 复制代码
import { buildProps } from "element-plus/es/utils/index.mjs";

export const ScrollTreeProps = buildProps({
  height: {
    type: [Number, String],
    default: "100%",
  },
  width: {
    type: [Number, String],
    default: "100%",
  },
} as const);

ScrollTree.vue

js 复制代码
<template>
  <div
    :class="wrapClassName"
    :style="{
      width: isNumber(props.width) ? props.width + 'px' : props.width,
      height: isNumber(props.height) ? props.height + 'px' : props.height,
      overflow: 'scroll',
    }"
  >
    <div
      :class="className"
      :style="{
        width: width,
        height: height,
      }"
      style="overflow: hidden"
    >
      <el-tree v-bind="$attrs">
        <!-- 遍历子组件作用域插槽,并对父组件暴露 -->
        <template v-for="(index, name) in $slots" v-slot:[name]="data">
          <slot :name="name" v-bind="data"></slot>
        </template>
      </el-tree>
    </div>
  </div>
</template>
<script lang="ts" setup>
import { defineProps, ref } from "vue";
import useResizeObserver from "../../../hooks/src/useResizeObserver";
import { ScrollTreeProps } from "./ScrollTree";
import * as _ from "lodash";
const { isNumber, uniqueId } = _;
//#region <实现>
const className = uniqueId("yh-scroll-tree");
const wrapClassName = uniqueId("yh-scroll-tree-wrap");
const props = defineProps(ScrollTreeProps);
// 应该设置的高度
const height = ref("100%");
const width = ref("100%");
const callback = (entries: ResizeObserverEntry[]) => {
  // 寻找高度
  const newHeight = entries?.[0].borderBoxSize?.[0]?.blockSize;
  height.value = newHeight ? newHeight + "px" : "100%";
  //   寻找宽度
  let maxWidth = 0;
  let maxPaddingLeft = 0;
  let checkBoxWidth = 0;
  const treeDom = entries?.[0]?.target;
  Array.from(treeDom?.children).forEach((item: HTMLElement) => {
    getWidth(item);
  });
  // 是否存在checkbox
  const checkbox = document.querySelector(
    `.${wrapClassName} .${className} .el-tree .el-tree-node__content .el-checkbox`
  ) as HTMLElement;
  if (checkbox) {
    const checkBoxMargin =
      +getElNodeAttrValue(checkbox, "margin-right")?.split("px")?.[0] || 0;
    const checkBoxClientWidth = checkbox.clientWidth;
    checkBoxWidth = checkBoxMargin + checkBoxClientWidth;
  }
  // 寻找最小宽度
  const minWidthNode = document.querySelector(`.${wrapClassName}`);
  const minWidth = minWidthNode?.clientWidth || 0;
  const targetWidth = maxWidth + maxPaddingLeft + checkBoxWidth;
  width.value = isNumber(targetWidth)
    ? (targetWidth > minWidth ? targetWidth : minWidth) + "px"
    : "100%";
  function getWidth(el: HTMLElement) {
    const elWidthNode = Array.from(el.children).find((item) => {
      return Array.from(item.classList || []).includes("el-tree-node__content");
    }) as HTMLElement;
    if (elWidthNode) {
      const paddingLeftValue =
        +getElNodeAttrValue(elWidthNode, "padding-left")?.split("px")?.[0] || 0;
      let elWidthNodeList = elWidthNode?.children || ([] as HTMLElement[]);
      let elWidth = 0;
      // 获取padding
      Array.from(elWidthNodeList).forEach((item) => {
        elWidth += item.clientWidth;
      });

      maxWidth = maxWidth > elWidth ? maxWidth : elWidth;
      maxPaddingLeft =
        maxPaddingLeft > paddingLeftValue ? maxPaddingLeft : paddingLeftValue;
    }

    if (el.ariaExpanded === "false") {
      return;
    }
    if (el.children) {
      Array.from(el.children).forEach((item: HTMLElement) => {
        getWidth(item);
      });
    }
  }
};
useResizeObserver(`.${wrapClassName} .${className} .el-tree`, callback, {
  throttleTime: 0,
});
const getElNodeAttrValue = (el: HTMLElement, attrKey: string) => {
  const computedStyles = getComputedStyle(el);
  return computedStyles.getPropertyValue(attrKey) as string;
};
//#endregion
</script>

三、实现效果

js 复制代码
<template>
  <div>
    <h2>直接使用设置宽高</h2>
    <ScrollTree :data="data" :props="props" :height="200" :width="500" />
    <h2>直接使用不设置宽高</h2>
    <ScrollTree :data="data" :props="props" />
    <h2>父组件设置宽高</h2>
    <div style="width: 500px; height: 200px">
      <ScrollTree :data="data" :props="props">
        <template #default="{ node }">
          <span>{{ node.label }}</span>
        </template>
      </ScrollTree>
    </div>
  </div>
</template>
<script lang="ts" setup>
import { ScrollTree } from "../../../packages";
interface Tree {
  id: string;
  label: string;
  children?: Tree[];
}

const getKey = (prefix: string, id: number) => {
  return `${prefix}-${id}-测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试--`;
};

const createData = (
  maxDeep: number,
  maxChildren: number,
  minNodesNumber: number,
  deep = 1,
  key = "node"
): Tree[] => {
  let id = 0;
  return Array.from({ length: minNodesNumber })
    .fill(deep)
    .map(() => {
      const childrenNumber =
        deep === maxDeep ? 0 : Math.round(Math.random() * maxChildren);
      const nodeKey = getKey(key, ++id);
      return {
        id: nodeKey,
        label: nodeKey,
        children: childrenNumber
          ? createData(maxDeep, maxChildren, childrenNumber, deep + 1, nodeKey)
          : undefined,
      };
    });
};

const props = {
  value: "id",
  label: "label",
  children: "children",
};
const data = createData(2, 3, 20);
</script>
<style scoped lang="scss"></style>
相关推荐
码蜂窝编程官方22 分钟前
【含开题报告+文档+PPT+源码】基于SpringBoot+Vue的虎鲸旅游攻略网的设计与实现
java·vue.js·spring boot·后端·spring·旅游
gqkmiss22 分钟前
Chrome 浏览器 131 版本开发者工具(DevTools)更新内容
前端·chrome·浏览器·chrome devtools
Summer不秃27 分钟前
Flutter之使用mqtt进行连接和信息传输的使用案例
前端·flutter
旭日猎鹰32 分钟前
Flutter踩坑记录(二)-- GestureDetector+Expanded点击无效果
前端·javascript·flutter
Viktor_Ye38 分钟前
高效集成易快报与金蝶应付单的方案
java·前端·数据库
hummhumm40 分钟前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
乐闻x1 小时前
Vue.js 性能优化指南:掌握 keep-alive 的使用技巧
前端·vue.js·性能优化
一条晒干的咸魚1 小时前
【Web前端】创建我的第一个 Web 表单
服务器·前端·javascript·json·对象·表单
花海少爷1 小时前
第十章 JavaScript的应用课后习题
开发语言·javascript·ecmascript
Amd7941 小时前
Nuxt.js 应用中的 webpack:compiled 事件钩子
前端·webpack·开发·编译·nuxt.js·事件·钩子