import type { DId } from '../../utils/types';
import type { ComboboxKeyDownRef } from '../_keyboard';
import type { DFormControl } from '../form';
import type { AbstractTreeNode } from './abstract-node';
import { isNull, isUndefined } from 'lodash';
import React, { useState, useMemo, useRef } from 'react';
import { useId } from '@react-devui/hooks';
import { findNested, getClassName } from '@react-devui/utils';
import { useGeneralContext, useDValue } from '../../hooks';
import { registerComponentMate } from '../../utils';
import { DFocusVisible } from '../_focus-visible';
import { useFormControl } from '../form';
import { useComponentConfig, usePrefixConfig } from '../root';
import { DPanel } from './Panel';
import { MultipleTreeNode } from './multiple-node';
import { SingleTreeNode } from './single-node';
export interface DTreeItem<V extends DId> {
label: string;
value: V;
loading?: boolean;
disabled?: boolean;
children?: DTreeItem<V>[];
}
export interface DTreeProps<V extends DId, T extends DTreeItem<V>> extends Omit<React.HTMLAttributes<HTMLElement>, 'children'> {
dFormControl?: DFormControl;
dList: T[];
dModel?: V | null | V[];
dHeight?: number;
dExpands?: V[];
dShowLine?: boolean;
dDisabled?: boolean;
dMultiple?: boolean;
dOnlyLeafSelectable?: boolean;
dCustomItem?: (item: T) => React.ReactNode;
onModelChange?: (value: any, item: any) => void;
onFirstExpand?: (value: T['value'], item: T) => void;
onExpandsChange?: (ids: T['value'][], items: (T | undefined)[]) => void;
}
const { COMPONENT_NAME } = registerComponentMate({ COMPONENT_NAME: 'DTree' as const });
export function DTree<V extends DId, T extends DTreeItem<V>>(props: DTreeProps<V, T>): JSX.Element | null {
const {
dFormControl,
dList,
dModel,
dHeight,
dExpands,
dShowLine = false,
dDisabled = false,
dMultiple = false,
dOnlyLeafSelectable = true,
dCustomItem,
onModelChange,
onFirstExpand,
onExpandsChange,
...restProps
} = useComponentConfig(COMPONENT_NAME, props);
const dPrefix = usePrefixConfig();
const { gDisabled } = useGeneralContext();
const comboboxKeyDownRef = useRef<ComboboxKeyDownRef>(null);
const dataRef = useRef<{
hasExpandList: Set<V>;
}>({
hasExpandList: new Set(dExpands),
});
const uniqueId = useId();
const getGroupId = (val: V) => `${dPrefix}tree-group-${val}-${uniqueId}`;
const getItemId = (val: V) => `${dPrefix}tree-item-${val}-${uniqueId}`;
const renderNodes = useMemo(
() =>
dList.map((item) =>
dMultiple
? new MultipleTreeNode(item, (origin) => origin.value, {
disabled: item.disabled,
})
: new SingleTreeNode(item, (origin) => origin.value, {
disabled: item.disabled,
})
),
[dMultiple, dList]
);
const nodesMap = useMemo(() => {
const nodes = new Map<V, AbstractTreeNode<V, T>>();
const reduceArr = (arr: AbstractTreeNode<V, T>[]) => {
for (const item of arr) {
nodes.set(item.id, item);
if (item.children) {
reduceArr(item.children);
}
}
};
reduceArr(renderNodes);
return nodes;
}, [renderNodes]);
const formControlInject = useFormControl(dFormControl);
const [_select, changeSelect] = useDValue<V | null | V[]>(
dMultiple ? [] : null,
dModel,
(value) => {
if (onModelChange) {
if (dMultiple) {
onModelChange(
value,
(value as V[]).map((v) => nodesMap.get(v)?.origin)
);
} else {
onModelChange(value, isNull(value) ? null : nodesMap.get(value as V)?.origin);
}
}
},
undefined,
formControlInject
);
const select = useMemo(() => (dMultiple ? new Set(_select as V[]) : (_select as V | null)), [_select, dMultiple]);
renderNodes.forEach((node) => {
node.updateStatus(select);
});
const [_expandIds, changeExpandIds] = useDValue<V[]>([], dExpands, (value) => {
if (onExpandsChange) {
onExpandsChange(
value,
value.map((v) => nodesMap.get(v)?.origin)
);
}
});
const expandIds = useMemo(() => new Set(_expandIds), [_expandIds]);
const disabled = dDisabled || gDisabled || dFormControl?.control.disabled;
const hasSelected = dMultiple ? (select as Set<V>).size > 0 : !isNull(select);
const [focusVisible, setFocusVisible] = useState(false);
const [_focusNode, setFocusNode] = useState<AbstractTreeNode<V, T> | undefined>();
const focusNode = (() => {
let node: AbstractTreeNode<V, T> | undefined;
if (_focusNode) {
node = nodesMap.get(_focusNode.id);
if (node && node.enabled) {
return node;
}
}
if (hasSelected) {
node = findNested(renderNodes, (node) => node.enabled && node.checked);
}
if (isUndefined(node)) {
node = findNested(renderNodes, (node) => node.enabled);
}
return node;
})();
return (
<DFocusVisible onFocusVisibleChange={setFocusVisible}>
{({ render: renderFocusVisible }) =>
renderFocusVisible(
<DPanel
{...restProps}
ref={comboboxKeyDownRef}
className={getClassName(restProps.className, {
'is-disabled': disabled,
})}
style={{
...restProps.style,
maxHeight: dHeight,
}}
tabIndex={restProps.tabIndex ?? (disabled ? -1 : 0)}
onKeyDown={(e) => {
restProps.onKeyDown?.(e);
switch (e.code) {
case 'ArrowUp':
e.preventDefault();
comboboxKeyDownRef.current?.('prev');
break;
case 'ArrowDown':
e.preventDefault();
comboboxKeyDownRef.current?.('next');
break;
case 'ArrowLeft':
e.preventDefault();
comboboxKeyDownRef.current?.('prev-level');
break;
case 'ArrowRight':
e.preventDefault();
comboboxKeyDownRef.current?.('next-level');
break;
case 'Home':
e.preventDefault();
comboboxKeyDownRef.current?.('first');
break;
case 'End':
e.preventDefault();
comboboxKeyDownRef.current?.('last');
break;
case 'Enter':
e.preventDefault();
comboboxKeyDownRef.current?.('click');
break;
default:
break;
}
}}
dGetGroupId={getGroupId}
dGetItemId={getItemId}
dList={renderNodes}
dExpandIds={expandIds}
dHeight={dHeight ?? Infinity}
dPadding={undefined}
dFocusItem={focusNode}
dCustomItem={dCustomItem}
dShowLine={dShowLine}
dMultiple={dMultiple}
dFocusVisible={focusVisible}
onFocusChange={setFocusNode}
onExpandChange={(item) => {
const isExpand = expandIds.has(item.id);
if (!item.origin.loading) {
if (isExpand) {
changeExpandIds((draft) => {
draft.splice(
draft.findIndex((id) => id === item.id),
1
);
});
} else {
if (!dataRef.current.hasExpandList.has(item.id)) {
dataRef.current.hasExpandList.add(item.id);
onFirstExpand?.(item.id, item.origin);
}
changeExpandIds((draft) => {
draft.push(item.id);
});
}
}
}}
onClickItem={(node) => {
if (dMultiple) {
const checkeds = (node as MultipleTreeNode<V, T>).changeStatus(node.checked ? 'UNCHECKED' : 'CHECKED', select as Set<V>);
changeSelect(Array.from(checkeds.keys()));
} else {
if (!dOnlyLeafSelectable || node.isLeaf) {
changeSelect(node.id);
}
}
}}
></DPanel>
)
}
</DFocusVisible>
);
}