【我要做开源】Vue DevUI开源指南09:实现tree组件禁止展开/收起、点选高亮和节点禁用功能

0 tree组件的现状回顾

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

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

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

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

1 禁止展开/收起

1.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,
}]
复制代码

测试功能正常!

1.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

完成!

1.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 // 新增
}
复制代码

2 点选高亮

2.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
复制代码

2.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>
  )
}
复制代码

3 节点禁选

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

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

3.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 : ''])
    )
}
复制代码

3.2 增加禁止样式

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

猜你喜欢

转载自juejin.im/post/7034814693197742087