123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466 |
- import React, { PureComponent } from 'react';
- import cls from 'classnames';
- import PropTypes from 'prop-types';
- import { cssClasses } from '@douyinfe/semi-foundation/tree/constants';
- import isEnterPress from '@douyinfe/semi-foundation/utils/isEnterPress';
- import { debounce, isFunction, isString, get, isEmpty } from 'lodash';
- import { IconTreeTriangleDown, IconFile, IconFolder, IconFolderOpen } from '@douyinfe/semi-icons';
- import { Checkbox } from '../checkbox';
- import TreeContext, { TreeContextValue } from './treeContext';
- import Spin from '../spin';
- import { TreeNodeProps, TreeNodeState } from './interface';
- import { getHighLightTextHTML } from '../_utils/index';
- import Indent from './indent';
- const prefixcls = cssClasses.PREFIX_OPTION;
- export default class TreeNode extends PureComponent<TreeNodeProps, TreeNodeState> {
- static contextType = TreeContext;
- static propTypes = {
- expanded: PropTypes.bool,
- selected: PropTypes.bool,
- checked: PropTypes.bool,
- halfChecked: PropTypes.bool,
- active: PropTypes.bool,
- disabled: PropTypes.bool,
- loaded: PropTypes.bool,
- loading: PropTypes.bool,
- isLeaf: PropTypes.bool,
- pos: PropTypes.string,
- children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
- icon: PropTypes.node,
- directory: PropTypes.bool,
- keyword: PropTypes.string,
- treeNodeFilterProp: PropTypes.string,
- selectedKey: PropTypes.string,
- motionKey: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
- isEnd: PropTypes.arrayOf(PropTypes.bool),
- showLine: PropTypes.bool
- };
- static defaultProps = {
- selectedKey: '',
- motionKey: '',
- };
- debounceSelect: any;
- refNode: HTMLElement;
- context: TreeContextValue;
- constructor(props: TreeNodeProps) {
- super(props);
- this.state = {};
- this.debounceSelect = debounce(this.onSelect, 500, {
- leading: true,
- trailing: false
- });
- }
- onSelect = (e: React.MouseEvent | React.KeyboardEvent) => {
- const { onNodeSelect } = this.context;
- onNodeSelect(e, this.props);
- };
- onExpand = (e: React.MouseEvent | React.KeyboardEvent) => {
- const { onNodeExpand } = this.context;
- e && e.stopPropagation();
- e.nativeEvent.stopImmediatePropagation();
- onNodeExpand(e, this.props);
- };
- onCheck = (e: React.MouseEvent | React.KeyboardEvent) => {
- if (this.isDisabled()) {
- return;
- }
- const { onNodeCheck } = this.context;
- e.stopPropagation();
- e.nativeEvent?.stopImmediatePropagation?.();
- onNodeCheck(e, this.props);
- };
- /**
- * A11y: simulate checkbox click
- */
- handleCheckEnterPress = (e: React.KeyboardEvent) => {
- if (isEnterPress(e)) {
- this.onCheck(e);
- }
- }
- onContextMenu = (e: React.MouseEvent) => {
- const { onNodeRightClick } = this.context;
- onNodeRightClick(e, this.props);
- };
- onClick = (e: React.MouseEvent | React.KeyboardEvent) => {
- const { expandAction } = this.context;
- if (expandAction === 'doubleClick') {
- this.debounceSelect(e);
- return;
- }
- this.onSelect(e);
- if (expandAction === 'click') {
- this.onExpand(e);
- }
- };
- /**
- * A11y: simulate li click
- */
- handleliEnterPress = (e: React.KeyboardEvent) => {
- if (isEnterPress(e)) {
- this.onClick(e);
- }
- }
- onDoubleClick = (e: React.MouseEvent) => {
- const { expandAction, onNodeDoubleClick } = this.context;
- e.stopPropagation();
- e.nativeEvent.stopImmediatePropagation();
- if (isFunction(onNodeDoubleClick)) {
- onNodeDoubleClick(e, this.props);
- }
- if (expandAction === 'doubleClick') {
- this.onExpand(e);
- }
- };
- onDragStart = (e: React.DragEvent<HTMLLIElement>) => {
- const { onNodeDragStart } = this.context;
- e.stopPropagation();
- onNodeDragStart(e, { ...this.props, nodeInstance: this.refNode });
- try {
- // ie throw error
- // firefox-need-it
- e.dataTransfer.setData('text/plain', '');
- } catch (error) {
- // empty
- }
- };
- onDragEnter = (e: React.DragEvent<HTMLLIElement>) => {
- const { onNodeDragEnter } = this.context;
- e.preventDefault();
- e.stopPropagation();
- onNodeDragEnter(e, { ...this.props, nodeInstance: this.refNode });
- };
- onDragOver = (e: React.DragEvent<HTMLLIElement>) => {
- const { onNodeDragOver } = this.context;
- e.preventDefault();
- e.stopPropagation();
- onNodeDragOver(e, { ...this.props, nodeInstance: this.refNode });
- };
- onDragLeave = (e: React.DragEvent<HTMLLIElement>) => {
- const { onNodeDragLeave } = this.context;
- e.stopPropagation();
- onNodeDragLeave(e, { ...this.props, nodeInstance: this.refNode });
- };
- onDragEnd = (e: React.DragEvent<HTMLLIElement>) => {
- const { onNodeDragEnd } = this.context;
- e.stopPropagation();
- onNodeDragEnd(e, { ...this.props, nodeInstance: this.refNode });
- };
- onDrop = (e: React.DragEvent<HTMLLIElement>) => {
- const { onNodeDrop } = this.context;
- e.preventDefault();
- e.stopPropagation();
- onNodeDrop(e, { ...this.props, nodeInstance: this.refNode });
- };
- getNodeChildren = () => {
- const { children } = this.props;
- return children || [];
- };
- isLeaf = () => {
- const { isLeaf, loaded } = this.props;
- const { loadData } = this.context;
- const hasChildren = this.getNodeChildren().length !== 0;
- if (isLeaf === false) {
- return false;
- }
- return isLeaf || (!loadData && !hasChildren) || (loadData && loaded && !hasChildren);
- };
- isDisabled = () => {
- const { disabled } = this.props;
- const { treeDisabled } = this.context;
- if (disabled === false) {
- return false;
- }
- return Boolean(treeDisabled || disabled);
- };
- renderArrow() {
- const showIcon = !this.isLeaf();
- const { loading, expanded, showLine } = this.props;
- if (loading) {
- return <Spin wrapperClassName={`${prefixcls}-spin-icon`} />;
- }
- if (showIcon) {
- return (
- <IconTreeTriangleDown
- role='button'
- aria-label={`${expanded ? 'Expand' : 'Collapse'} the tree item`}
- className={`${prefixcls}-expand-icon`}
- size="small"
- onClick={this.onExpand}
- />
- );
- }
- if (showLine) {
- return this.renderSwitcher();
- }
- return (
- <span className={`${prefixcls}-empty-icon`} />
- );
- }
- renderCheckbox() {
- const { checked, halfChecked, eventKey } = this.props;
- const disabled = this.isDisabled();
- return (
- <div
- role='none'
- onClick={this.onCheck}
- onKeyPress={this.handleCheckEnterPress}
- >
- <Checkbox
- aria-label='Toggle the checked state of checkbox'
- value={eventKey}
- indeterminate={halfChecked}
- checked={checked}
- disabled={Boolean(disabled)}
- />
- </div>
- );
- }
- // Switcher
- renderSwitcher = () => {
- if (this.isLeaf()) {
- // if switcherIconDom is null, no render switcher span
- return (<span className={cls(`${prefixcls}-switcher`)} >
- <span className={`${prefixcls}-switcher-leaf-line`} />
- </span>);
- }
- return null;
- };
- renderIcon() {
- const {
- directory,
- treeIcon
- } = this.context;
- const { expanded, icon } = this.props;
- const hasChild = !this.isLeaf();
- const hasIcon = icon || treeIcon;
- let itemIcon;
- if (hasIcon || directory) {
- if (hasIcon) {
- itemIcon = icon || treeIcon;
- } else {
- if (!hasChild) {
- itemIcon = <IconFile className={`${prefixcls}-item-icon`} />;
- } else {
- itemIcon = expanded ? <IconFolderOpen className={`${prefixcls}-item-icon`} /> : <IconFolder className={`${prefixcls}-item-icon`} />;
- }
- }
- }
- return itemIcon;
- }
- renderEmptyNode() {
- const { emptyContent } = this.props;
- const wrapperCls = cls(prefixcls, {
- [`${prefixcls}-empty`]: true,
- });
- return (
- <ul className={wrapperCls}>
- <li className={`${prefixcls}-label ${prefixcls}-label-empty`} x-semi-prop="emptyContent">
- {emptyContent}
- </li>
- </ul>
- );
- }
- renderRealLabel = () => {
- const { renderLabel } = this.context;
- const { label, keyword, data, filtered, treeNodeFilterProp } = this.props;
- if (isFunction(renderLabel)) {
- return renderLabel(label, data);
- } else if (isString(label) && filtered && keyword) {
- return getHighLightTextHTML({
- sourceString: label,
- searchWords: [keyword],
- option: {
- highlightTag: 'span',
- highlightClassName: `${prefixcls}-highlight`,
- },
- } as any);
- } else {
- return label;
- }
- };
- setRef = (node: HTMLElement) => {
- this.refNode = node;
- };
- render() {
- const {
- eventKey,
- expanded,
- selected,
- checked,
- halfChecked,
- loading,
- active,
- level,
- empty,
- filtered,
- treeNodeFilterProp,
- display,
- style,
- isEnd,
- showLine,
- ...rest
- } = this.props;
- if (empty) {
- return this.renderEmptyNode();
- }
- const {
- multiple,
- draggable,
- renderFullLabel,
- dragOverNodeKey,
- dropPosition,
- labelEllipsis
- } = this.context;
- const isEndNode = isEnd[isEnd.length - 1];
- const disabled = this.isDisabled();
- const dragOver = dragOverNodeKey === eventKey && dropPosition === 0;
- const dragOverGapTop = dragOverNodeKey === eventKey && dropPosition === -1;
- const dragOverGapBottom = dragOverNodeKey === eventKey && dropPosition === 1;
- const nodeCls = cls(prefixcls, {
- [`${prefixcls}-level-${level + 1}`]: true,
- [`${prefixcls}-fullLabel-level-${level + 1}`]: renderFullLabel,
- [`${prefixcls}-collapsed`]: !expanded,
- [`${prefixcls}-disabled`]: Boolean(disabled),
- [`${prefixcls}-selected`]: selected,
- [`${prefixcls}-active`]: !multiple && active,
- [`${prefixcls}-ellipsis`]: labelEllipsis,
- [`${prefixcls}-drag-over`]: !disabled && dragOver,
- [`${prefixcls}-draggable`]: !disabled && draggable && !renderFullLabel,
- // When draggable + renderFullLabel is enabled, the default style
- [`${prefixcls}-fullLabel-draggable`]: !disabled && draggable && renderFullLabel,
- // When draggable + renderFullLabel is turned on, the style of dragover
- [`${prefixcls}-fullLabel-drag-over-gap-top`]: !disabled && dragOverGapTop && renderFullLabel,
- [`${prefixcls}-fullLabel-drag-over-gap-bottom`]: !disabled && dragOverGapBottom && renderFullLabel,
- [`${prefixcls}-tree-node-last-leaf`]: isEndNode,
- });
- const labelProps = {
- onClick: this.onClick,
- onContextMenu: this.onContextMenu,
- onDoubleClick: this.onDoubleClick,
- className: nodeCls,
- onExpand: this.onExpand,
- data: rest.data,
- level,
- onCheck: this.onCheck,
- style,
- expandIcon: this.renderArrow(),
- checkStatus: {
- checked,
- halfChecked,
- },
- expandStatus: {
- expanded,
- loading,
- },
- filtered,
- searchWord: rest.keyword,
- };
- const dragProps = {
- onDoubleClick: this.onDoubleClick,
- onDragStart: draggable ? this.onDragStart : undefined,
- onDragEnter: draggable ? this.onDragEnter : undefined,
- onDragOver: draggable ? this.onDragOver : undefined,
- onDragLeave: draggable ? this.onDragLeave : undefined,
- onDrop: draggable ? this.onDrop : undefined,
- onDragEnd: draggable ? this.onDragEnd : undefined,
- draggable: (!disabled && draggable) || undefined,
- };
- if (renderFullLabel) {
- const customLabel = renderFullLabel({ ...labelProps });
- if (draggable) {
- // @ts-ignore skip cloneElement type check
- return React.cloneElement(customLabel, {
- ref: this.setRef,
- ...dragProps
- });
- } else {
- if (isEmpty(style)) {
- return customLabel;
- } else {
- // In virtualization, props.style will contain location information
- // @ts-ignore skip cloneElement type check
- return React.cloneElement(customLabel, {
- style: { ...get(customLabel, ['props', 'style']), ...style }
- });
- }
- }
- }
- const labelCls = cls(`${prefixcls}-label`, {
- [`${prefixcls}-drag-over-gap-top`]: !disabled && dragOverGapTop,
- [`${prefixcls}-drag-over-gap-bottom`]: !disabled && dragOverGapBottom,
- });
- const setsize = get(rest, ['data', 'children', 'length']);
- const posinset = isString(rest.pos) ? Number(rest.pos.split('-')[level + 1]) + 1 : 1;
- return (
- <li
- className={nodeCls}
- role="treeitem"
- aria-disabled={disabled}
- aria-checked={checked}
- aria-selected={selected}
- aria-setsize={setsize}
- aria-posinset={posinset}
- aria-expanded={expanded}
- aria-level={level + 1}
- data-key={eventKey}
- onClick={this.onClick}
- onKeyPress={this.handleliEnterPress}
- onContextMenu={this.onContextMenu}
- onDoubleClick={this.onDoubleClick}
- ref={this.setRef}
- style={style}
- {...dragProps}
- >
- <Indent showLine={showLine} prefixcls={prefixcls} level={level} isEnd={isEnd} />
- {this.renderArrow()}
- <span
- className={labelCls}
- >
- {multiple ? this.renderCheckbox() : null}
- {this.renderIcon()}
- <span className={`${prefixcls}-label-text`}>{this.renderRealLabel()}</span>
- </span>
- </li>
- );
- }
- }
|