Skip to content

从0到1搭建Vue组件库11:实现tree组件禁止展开/收起、点选高亮和节点禁用功能

image

0 tree组件的现状回顾

我要做开源系列直播到目前已经做了8期:

  • 1-3期分享tree组件的设计和实现
  • 4-8期分享组件库工程化,并实现一个五脏六腑俱全的mini-vue-devui组件库

到第8期为止,组件库的基础框架搭建(基于Vite+Vue3+TypeScript+JSX,并完成monorepo改造)、文档系统、单元测试、按需构建和发布流程已经全部打通,意味着我们可以基于此不断完善组件,也意味着我们可以暂时将组件库工程化的事务放一旁,继续开发tree组件。

我们先来看下之前的tree组件进展:

本期主要完善tree组件的以下功能:

  1. 完善tree组件样式
  2. 禁止展开/收起
  3. 点选高亮
  4. 节点禁选

1 完善tree组件样式

tree.scss

$devui-text-weak: var(--devui-text-weak, #575d6c);
$devui-font-size: var(--devui-font-size, 12px);
$devui-list-item-selected-bg: var(--devui-list-item-selected-bg, #e9edfa);
$devui-list-item-hover-bg: var(--devui-list-item-hover-bg, #f2f5fc);
$devui-border-radius: var(--devui-border-radius, 2px);
$devui-animation-duration-fast: var(--devui-animation-duration-fast, 100ms);
$devui-animation-ease-in-smooth: var(--devui-animation-ease-in-smooth, cubic-bezier(0.645, 0.045, 0.355, 1));

.devui-text-ellipsis {
  text-overflow: ellipsis;
  overflow: hidden;
  white-space: nowrap;
}

.devui-tree-node {
  color: $devui-text-weak;
  line-height: 1.5;
  white-space: nowrap;
  position: relative;

  .devui-tree-node__content {
    display: inline-flex;
    align-items: center;
    font-size: $devui-font-size;
    padding-right: 10px;
    width: 100%;
    border-radius: $devui-border-radius;
    padding-left: 6px;
    transition: color $devui-animation-duration-fast $devui-animation-ease-in-smooth, background-color $devui-animation-duration-fast $devui-animation-ease-in-smooth;

    &.active {
      background-color: $devui-list-item-selected-bg;
      text-decoration: none;
      border-color: transparent;
    }

    &:not(.active):hover {
      background-color: $devui-list-item-hover-bg;
    }
  }

  .devui-tree-node__content--value-wrapper {
    display: inline-flex;
    align-items: center;
    height: 30px;
    width: 100%;
  }

  .devui-tree-node__title {
    @extend .devui-text-ellipsis;

    margin-left: 5px;
    display: inline-block;
    border: 1px dashed transparent;
    border-radius: $devui-border-radius;
    max-width: 100%;

    &:not(.disabled) {
      cursor: pointer;
    }
  }
}

2 禁止展开/收起

2.1 增加禁止展开/收起的功能

packages/devui-vue/devui/tree/src/composables/use-toggle.ts

const toggle = (item: TreeItem) => {
  if (!item.children) return
  if (item.disableToggle) return // 新增
  item.open = !item.open
  openedData.value = openedTree(data)
}

在传入tree组件的data数据中增加disableToggle数据

[{
  label: '一级 1', level: 1,
  children: [{
    label: '二级 1-1', level: 2,
    children: [{
      label: '三级 1-1-1', level: 3,
    }]
  }]
}, {
  label: '一级 2', level: 1,
  open: true,
  children: [{
    label: '二级 2-1', level: 2,
    children: [{
      label: '三级 2-1-1', level: 3,
    }]
  }, {
    label: '二级 2-2', level: 2,
    disableToggle: true, // 新增
    children: [{
      label: '三级 2-2-1', level: 3,
    }]
  }]
}, {
  label: '一级 3', level: 1,
  open: true,
  children: [{
    label: '二级 3-1', level: 2,
    children: [{
      label: '三级 3-1-1', level: 3,
    }]
  }, {
    label: '二级 3-2', level: 2,
    open: true,
    children: [{
      label: '三级 3-2-1', level: 3,
    }]
  }]
}, {
  label: '一级 4', level: 1,
}]

测试功能正常!

2.2 增加禁止展开/收起的样式

packages/devui-vue/devui/tree/src/tree.ts

const renderNode = (item: TreeItem) => {
  return (
    <div
      class={['devui-tree-node', item.open && 'devui-tree-node__open']}
      style={{ paddingLeft: `${24 * (item.level - 1)}px` }}
    >
      <div class="devui-tree-node__content">
        <div class="devui-tree-node__content--value-wrapper">
          {
            item.children
              ? <span class={item.disableToggle && 'toggle-disabled'}> // 增加
                  {
                    item.open
                      ? <IconOpen class='mr-xs' onClick={() => toggle(item)} />
                      : <IconClose class='mr-xs' onClick={() => toggle(item)} />
                  }
                </span>
              : <Indent />
          }
          <span class="devui-tree-node__title">{item.label}</span>
        </div>
      </div>
    </div>
  )
}

增加样式

$devui-disabled-text: var(--devui-disabled-text, #adb0b8);

.toggle-disabled {
  cursor: not-allowed;

  svg.svg-icon rect {
    stroke: $devui-disabled-text;
  }

  svg.svg-icon path {
    fill: $devui-disabled-text;
  }
}

效果如下:

image.png

完成!

2.3 代码重构

抽离渲染节点前面的图标的逻辑:

const renderIcon = (item: TreeItem) => {
  return item.children
    ? <span class={item.disableToggle && 'toggle-disabled'}>
        {
          item.open
            ? <IconOpen class='mr-xs' onClick={() => toggle(item)} />
            : <IconClose class='mr-xs' onClick={() => toggle(item)} />
        }
      </span>
    : <Indent />
}

renderNode方法中用renderIcon替换相应的代码:

<div class="devui-tree-node__content--value-wrapper">
  { renderIcon(item) }
  <span class="devui-tree-node__title">{item.label}</span>
</div>

别忘了在tree-types.ts中增加类型:

export interface TreeItem {
  label: string
  children?: TreeData
  disableToggle?: boolean // 新增
}

3 点选高亮

3.1 实现 useHighlightNode 这个 composable

增加use-highlight.ts文件:

import { ref, Ref } from 'vue'

interface TypeHighlightClass {
  [key: string]: 'active' | '' | 'devui-tree_isDisabledNode'
}
type TypeUseHighlightNode = () => {
  nodeClassNameReflect: Ref<TypeHighlightClass>
  handleClickOnNode: (index: string) => void
  handleInitNodeClassNameReflect: (isDisabled: boolean, ...keys: Array<string>) => string
}

const HIGHLIGHT_CLASS = 'active'
const IS_DISABLED_FLAG = 'devui-tree_isDisabledNode'
const useHighlightNode: TypeUseHighlightNode = () => {
  const nodeClassNameReflectRef = ref<TypeHighlightClass>({})
  const handleInit = (isDisabled = false, ...keys) => {
    const key = keys.join('-')
    nodeClassNameReflectRef.value[key] = isDisabled ? IS_DISABLED_FLAG : (nodeClassNameReflectRef.value[key] || '')
    return key
  }
  const handleClick = (key) => {
    if (nodeClassNameReflectRef.value[key] === IS_DISABLED_FLAG) {
      return
    }
    nodeClassNameReflectRef.value =
      Object.fromEntries(
        Object
          .entries(nodeClassNameReflectRef.value)
          .map(([k]) => [k, k === key ? HIGHLIGHT_CLASS : ''])
      )
  }
  return {
    nodeClassNameReflect: nodeClassNameReflectRef,
    handleClickOnNode: handleClick,
    handleInitNodeClassNameReflect: handleInit,
  }
}
export default useHighlightNode

3.2 在 setup 中使用 useHighlightNode

tree.tsx中使用use-highlight.ts这个composable

const { nodeClassNameReflect, handleInitNodeClassNameReflect, handleClickOnNode } = useHighlightNode()
const renderNode = (item: TreeItem) => {
  const { key = '', label, disabled, open, level, children } = item
  const nodeId = handleInitNodeClassNameReflect(disabled, key, label) // 获取nodeId

  return (
    <div
      class={['devui-tree-node', open && 'devui-tree-node__open']}
      style={{ paddingLeft: `${24 * (level - 1)}px` }}
    >
      <div
        class={`devui-tree-node__content ${nodeClassNameReflect.value[nodeId]}`} // 增加高亮样式
        onClick={() => handleClickOnNode(nodeId)} // 增加节点的点击事件
      >
        <div class="devui-tree-node__content--value-wrapper">
          { renderIcon(item) }
          <span class="devui-tree-node__title">{item.label}</span>
        </div>
      </div>
    </div>
  )
}

4 节点禁选

和禁止展开/收起功能类似,分成两步:

  1. 增加禁止逻辑
  2. 增加禁止样式

4.1 增加禁止逻辑

const handleClick = (key) => {
  // 新增
  if (nodeClassNameReflectRef.value[key] === IS_DISABLED_FLAG) {
    return
  }
  
  nodeClassNameReflectRef.value =
    Object.fromEntries(
      Object
        .entries(nodeClassNameReflectRef.value)
        .map(([k]) => [k, k === key ? HIGHLIGHT_CLASS : ''])
    )
}

4.2 增加禁止样式

<div class="devui-tree-node__content--value-wrapper">
  { renderIcon(item) }
  <span class={['devui-tree-node__title', item.disabled && 'select-disabled']}> // 新增
    { label }
  </span>
</div>

发布时间:

Made with ❤ by