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>
相关推荐
mldong5 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排5 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60616 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角7 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!7 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰7 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi9 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen9 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马10 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu10 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss