All files / src/tree TreeNode.tsx

48.57% Statements 17/35
70% Branches 21/30
66.67% Functions 6/9
48.57% Lines 17/35

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240                                                                                                                                1x   13x     21x                                                         21x 21x 21x   21x                                                                                                                                         21x       21x 21x 21x                                                   21x               21x       21x       21x                   1x 1x                  
import React, { useState } from "react";
import classNames from "classnames";
import { TreeContext, TreeContextValue } from "./TreeContext";
import { Check } from "../check";
import { Icon } from "../icon";
import { StyledProps } from "../_type";
import { withStatics } from "../_util/with-statics";
import { injectValue } from "../_util/inject-value";
import { useConfig } from "../_util/config-context";
 
export interface TreeNodeProps extends StyledProps {
  /**
   * 节点 ID,整棵树中唯一
   */
  id: string;
 
  /**
   * 节点内容
   */
  content: React.ReactNode;
 
  /**
   * 节点图标
   *
   * @docType React.ReactNode | (context: { expanded: boolean }) => React.ReactNode
   */
  icon?:
    | ((context: { expanded: boolean }) => React.ReactNode)
    | React.ReactNode;
 
  /**
   * hover 后展示的操作
   *
   * 推荐使用文字(TreeNode.ActionLink),若超过一项,建议收至 Dropdown 下
   */
  operation?: React.ReactNode;
 
  /**
   * 当树为 selectable 时,设置当前节点是否展示 Checkbox
   *
   * @default true
   */
  selectable?: boolean;
 
  /**
   * 当树为 selectable 时,设置当前节点 Checkbox 是否禁用
   *
   * @default false
   */
  disableSelect?: boolean;
 
  /**
   * 当前节点是否支持展开(用于异步加载)
   *
   * @default false
   */
  expandable?: boolean;
 
  /**
   * 包含的树节点 <TreeNode />
   */
  children?: React.ReactNode;
}
 
export const TreeNode = withStatics(
  function TreeNode(props: TreeNodeProps) {
    return (
      <TreeContext.Consumer>
        {context => {
          return <TreeNodeInner {...context} {...props} />;
        }}
      </TreeContext.Consumer>
    );
  },
  {
    ActionLink,
  }
);
 
export function TreeNodeInner({
  id,
  content,
  icon,
  operation,
  selectable,
  activable,
  activeIds,
  onActive,
  expandable,
  children,
  expandedIds,
  onExpand,
  onLoad,
  onLoadError,
  switcherIcon,
  className,
  style,
}: TreeNodeProps & TreeContextValue) {
  const { classPrefix } = useConfig();
  const expanded = expandedIds.includes(id);
  const nodeIcon = injectValue(icon)({ expanded });
 
  return (
    <li
      className={classNames(`${classPrefix}-tree__node`, className, {
        // 高亮
        "is-selected": activable && activeIds.includes(id),
      })}
      style={style}
    >
      <div
        className={`${classPrefix}-tree__node-content`}
        style={activable ? { cursor: "pointer" } : undefined}
        onClick={() => activable && onActive(id)}
      >
        <TreeNodeSwitcher
          id={id}
          icon={switcherIcon}
          enable={Boolean(expandable || children)}
          expanded={expanded}
          onExpand={onExpand}
          onLoad={onLoad}
          onLoadError={onLoadError}
          classPrefix={classPrefix}
        />
 
        <div className={`${classPrefix}-tree__label`}>
          {selectable ? (
            <Check name={id} type="checkbox">
              {nodeIcon}
              {content}
            </Check>
          ) : (
            <>
              {nodeIcon}
              <span className={`${classPrefix}-tree__label-title`}>
                {content}
              </span>
            </>
          )}
        </div>
        {operation && (
          <div className={`${classPrefix}-tree__action`}>{operation}</div>
        )}
      </div>
      {expanded && (
        <ul className={`${classPrefix}-tree__subtree`}>{children}</ul>
      )}
    </li>
  );
}
 
interface TreeNodeSwitcherProps {
  id: TreeNodeProps["id"];
  icon: TreeContextValue["switcherIcon"];
  enable: boolean;
  expanded: boolean;
  onExpand: TreeContextValue["onExpand"];
  onLoad: TreeContextValue["onLoad"];
  onLoadError: TreeContextValue["onLoadError"];
  classPrefix: string;
}
 
function TreeNodeSwitcher({
  id,
  enable,
  expanded,
  onExpand,
  onLoad,
  onLoadError = () => null,
  icon = ({ expanded }) => (
    <Icon type={expanded ? "arrowdown" : "arrowright"} />
  ),
  classPrefix,
}: TreeNodeSwitcherProps) {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(false);
  const switcherIcon = injectValue(icon)({ expanded });
 
  async function handleExpand(event: React.MouseEvent) {
    event.stopPropagation();
    if (expanded) {
      onExpand(id, false);
      return;
    }
 
    if (onLoad) {
      setError(false);
      setLoading(true);
      try {
        await onLoad(id);
        onExpand(id, true);
      } catch (err) {
        setError(true);
        onLoadError(id, err);
      } finally {
        setLoading(false);
      }
    } else {
      onExpand(id, true);
    }
  }
 
  let switcher = React.isValidElement<any>(switcherIcon) ? (
    React.cloneElement(switcherIcon, {
      onClick: handleExpand,
    })
  ) : (
    <div onClick={handleExpand}>{switcherIcon}</div>
  );
 
  Iif (loading) {
    switcher = <Icon type="loading" />;
  }
 
  Iif (error) {
    switcher = <Icon type="error" onClick={handleExpand} />;
  }
 
  return (
    <a className={`${classPrefix}-tree__switcher`}>{enable && switcher}</a>
  );
}
 
function ActionLink({
  children,
  className,
  ...props
}: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
  const { classPrefix } = useConfig();
  return (
    <a
      className={classNames(`${classPrefix}-text--link`, className)}
      {...props}
    >
      {children}
    </a>
  );
}