feat: replace react-dnd to dnd-kit (#2028)
Co-authored-by: Himself65 <himself65@outlook.com>
This commit is contained in:
@@ -4,7 +4,6 @@
|
|||||||
import 'fake-indexeddb/auto';
|
import 'fake-indexeddb/auto';
|
||||||
|
|
||||||
import { rootCurrentWorkspaceIdAtom } from '@affine/workspace/atom';
|
import { rootCurrentWorkspaceIdAtom } from '@affine/workspace/atom';
|
||||||
import type { PageMeta } from '@blocksuite/store';
|
|
||||||
import matchers from '@testing-library/jest-dom/matchers';
|
import matchers from '@testing-library/jest-dom/matchers';
|
||||||
import type { RenderResult } from '@testing-library/react';
|
import type { RenderResult } from '@testing-library/react';
|
||||||
import { render, renderHook } from '@testing-library/react';
|
import { render, renderHook } from '@testing-library/react';
|
||||||
@@ -95,7 +94,6 @@ const initPinBoard = async () => {
|
|||||||
const app = render(
|
const app = render(
|
||||||
<App
|
<App
|
||||||
blockSuiteWorkspace={blockSuiteWorkspace as BlockSuiteWorkspace}
|
blockSuiteWorkspace={blockSuiteWorkspace as BlockSuiteWorkspace}
|
||||||
allMetas={blockSuiteWorkspace.meta.pageMetas as PageMeta[]}
|
|
||||||
openPage={() => {}}
|
openPage={() => {}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
export const StyledCollapsedButton = styled('button')<{
|
export const StyledCollapsedButton = styled('button')<{
|
||||||
collapse: boolean;
|
collapse: boolean;
|
||||||
show?: boolean;
|
show?: boolean;
|
||||||
}>(({ collapse, show = true, theme }) => {
|
}>(({ collapse, show = true }) => {
|
||||||
return {
|
return {
|
||||||
width: '16px',
|
width: '16px',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
@@ -43,7 +43,6 @@ export const StyledPinboard = styled('div')<{
|
|||||||
disableCollapse,
|
disableCollapse,
|
||||||
disable = false,
|
disable = false,
|
||||||
active = false,
|
active = false,
|
||||||
theme,
|
|
||||||
isOver,
|
isOver,
|
||||||
textWrap = false,
|
textWrap = false,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -66,7 +65,7 @@ export const StyledPinboard = styled('div')<{
|
|||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
...(textWrap
|
...(textWrap
|
||||||
? {
|
? {
|
||||||
wordBreak: 'break-all',
|
wordBreak: 'break-word',
|
||||||
whiteSpace: 'pre-wrap',
|
whiteSpace: 'pre-wrap',
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
@@ -106,7 +105,7 @@ export const StyledOperationButton = styled(IconButton, {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export const StyledSearchContainer = styled('div')(({ theme }) => {
|
export const StyledSearchContainer = styled('div')(() => {
|
||||||
return {
|
return {
|
||||||
width: 'calc(100% - 24px)',
|
width: 'calc(100% - 24px)',
|
||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
@@ -125,7 +124,7 @@ export const StyledMenuContent = styled('div')(() => {
|
|||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
export const StyledMenuSubTitle = styled('div')(({ theme }) => {
|
export const StyledMenuSubTitle = styled('div')(() => {
|
||||||
return {
|
return {
|
||||||
color: 'var(--affine-text-secondary-color)',
|
color: 'var(--affine-text-secondary-color)',
|
||||||
lineHeight: '36px',
|
lineHeight: '36px',
|
||||||
@@ -133,7 +132,7 @@ export const StyledMenuSubTitle = styled('div')(({ theme }) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export const StyledMenuFooter = styled('div')(({ theme }) => {
|
export const StyledMenuFooter = styled('div')(() => {
|
||||||
return {
|
return {
|
||||||
width: 'calc(100% - 24px)',
|
width: 'calc(100% - 24px)',
|
||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
export const StyledListItem = styled('div')<{
|
export const StyledListItem = styled('div')<{
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}>(({ theme, active, disabled }) => {
|
}>(({ active, disabled }) => {
|
||||||
return {
|
return {
|
||||||
height: '32px',
|
height: '32px',
|
||||||
color: active
|
color: active
|
||||||
@@ -46,7 +46,7 @@ export const StyledListItem = styled('div')<{
|
|||||||
export const StyledCollapseButton = styled('button')<{
|
export const StyledCollapseButton = styled('button')<{
|
||||||
collapse: boolean;
|
collapse: boolean;
|
||||||
show?: boolean;
|
show?: boolean;
|
||||||
}>(({ collapse, show = true, theme }) => {
|
}>(({ collapse, show = true }) => {
|
||||||
return {
|
return {
|
||||||
width: '16px',
|
width: '16px',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
@@ -75,7 +75,7 @@ export const StyledCollapseItem = styled('div')<{
|
|||||||
active?: boolean;
|
active?: boolean;
|
||||||
isOver?: boolean;
|
isOver?: boolean;
|
||||||
textWrap?: boolean;
|
textWrap?: boolean;
|
||||||
}>(({ disable = false, active = false, theme, isOver, textWrap = false }) => {
|
}>(({ disable = false, active = false, isOver, textWrap = false }) => {
|
||||||
return {
|
return {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
lineHeight: '1.5',
|
lineHeight: '1.5',
|
||||||
@@ -94,7 +94,7 @@ export const StyledCollapseItem = styled('div')<{
|
|||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
...(textWrap
|
...(textWrap
|
||||||
? {
|
? {
|
||||||
wordBreak: 'break-all',
|
wordBreak: 'break-word',
|
||||||
whiteSpace: 'pre-wrap',
|
whiteSpace: 'pre-wrap',
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
@@ -174,7 +174,7 @@ const slideOut2 = keyframes({
|
|||||||
|
|
||||||
export const StyledChangeLog = styled('div')<{
|
export const StyledChangeLog = styled('div')<{
|
||||||
isClose?: boolean;
|
isClose?: boolean;
|
||||||
}>(({ theme, isClose }) => {
|
}>(({ isClose }) => {
|
||||||
return {
|
return {
|
||||||
width: '110%',
|
width: '110%',
|
||||||
height: '32px',
|
height: '32px',
|
||||||
|
|||||||
@@ -41,8 +41,6 @@
|
|||||||
"lit": "^2.7.2",
|
"lit": "^2.7.2",
|
||||||
"lottie-web": "^5.11.0",
|
"lottie-web": "^5.11.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dnd": "^16.0.1",
|
|
||||||
"react-dnd-html5-backend": "^16.0.1",
|
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-error-boundary": "^4.0.3",
|
"react-error-boundary": "^4.0.3",
|
||||||
"react-is": "^18.2.0"
|
"react-is": "^18.2.0"
|
||||||
|
|||||||
@@ -1,147 +1,43 @@
|
|||||||
import { useEffect } from 'react';
|
import { useDraggable } from '@dnd-kit/core';
|
||||||
import { useDrag, useDrop } from 'react-dnd';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
StyledCollapse,
|
StyledCollapse,
|
||||||
StyledNodeLine,
|
|
||||||
StyledTreeNodeContainer,
|
StyledTreeNodeContainer,
|
||||||
StyledTreeNodeWrapper,
|
StyledTreeNodeWrapper,
|
||||||
} from './styles';
|
} from './styles';
|
||||||
import type {
|
import { NodeLine, TreeNodeItem, TreeNodeItemWithDnd } from './TreeNodeInner';
|
||||||
Node,
|
import type { TreeNodeProps } from './types';
|
||||||
NodeLIneProps,
|
|
||||||
TreeNodeItemProps,
|
|
||||||
TreeNodeProps,
|
|
||||||
} from './types';
|
|
||||||
|
|
||||||
const NodeLine = <RenderProps,>({
|
|
||||||
node,
|
|
||||||
onDrop,
|
|
||||||
allowDrop = true,
|
|
||||||
isTop = false,
|
|
||||||
}: NodeLIneProps<RenderProps>) => {
|
|
||||||
const [{ isOver }, drop] = useDrop(
|
|
||||||
() => ({
|
|
||||||
accept: 'node',
|
|
||||||
drop: (item: Node<RenderProps>, monitor) => {
|
|
||||||
const didDrop = monitor.didDrop();
|
|
||||||
if (didDrop) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onDrop?.(item.id, node.id, {
|
|
||||||
internal: false,
|
|
||||||
topLine: isTop,
|
|
||||||
bottomLine: !isTop,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
collect: monitor => ({
|
|
||||||
isOver: monitor.isOver(),
|
|
||||||
canDrop: monitor.canDrop(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
[onDrop]
|
|
||||||
);
|
|
||||||
|
|
||||||
return <StyledNodeLine ref={drop} show={isOver && allowDrop} isTop={isTop} />;
|
|
||||||
};
|
|
||||||
const TreeNodeItemWithDnd = <RenderProps,>({
|
|
||||||
node,
|
|
||||||
allowDrop,
|
|
||||||
setCollapsed,
|
|
||||||
...otherProps
|
|
||||||
}: TreeNodeItemProps<RenderProps>) => {
|
|
||||||
const { onAdd, onDelete, onDrop } = otherProps;
|
|
||||||
|
|
||||||
const [{ canDrop, isOver }, drop] = useDrop(
|
|
||||||
() => ({
|
|
||||||
accept: 'node',
|
|
||||||
drop: (item: Node<RenderProps>, monitor) => {
|
|
||||||
const didDrop = monitor.didDrop();
|
|
||||||
if (didDrop || item.id === node.id || !allowDrop) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onDrop?.(item.id, node.id, {
|
|
||||||
internal: true,
|
|
||||||
topLine: false,
|
|
||||||
bottomLine: false,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
collect: monitor => ({
|
|
||||||
isOver: monitor.isOver(),
|
|
||||||
canDrop: monitor.canDrop() && allowDrop,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
[onDrop, allowDrop]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOver && canDrop) {
|
|
||||||
setCollapsed(node.id, false);
|
|
||||||
}
|
|
||||||
}, [isOver, canDrop, setCollapsed, node.id]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TreeNodeItem
|
|
||||||
dropRef={drop}
|
|
||||||
onAdd={onAdd}
|
|
||||||
onDelete={onDelete}
|
|
||||||
node={node}
|
|
||||||
allowDrop={allowDrop}
|
|
||||||
setCollapsed={setCollapsed}
|
|
||||||
isOver={isOver}
|
|
||||||
canDrop={canDrop}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const TreeNodeItem = <RenderProps,>({
|
|
||||||
node,
|
|
||||||
collapsed,
|
|
||||||
setCollapsed,
|
|
||||||
selectedId,
|
|
||||||
isOver = false,
|
|
||||||
canDrop = false,
|
|
||||||
onAdd,
|
|
||||||
onDelete,
|
|
||||||
dropRef,
|
|
||||||
disableCollapse,
|
|
||||||
}: TreeNodeItemProps<RenderProps>) => {
|
|
||||||
return (
|
|
||||||
<div ref={dropRef}>
|
|
||||||
{node.render?.(node, {
|
|
||||||
isOver: isOver && canDrop,
|
|
||||||
onAdd: () => onAdd?.(node.id),
|
|
||||||
onDelete: () => onDelete?.(node.id),
|
|
||||||
collapsed,
|
|
||||||
setCollapsed,
|
|
||||||
isSelected: selectedId === node.id,
|
|
||||||
disableCollapse,
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TreeNodeWithDnd = <RenderProps,>(
|
export const TreeNodeWithDnd = <RenderProps,>(
|
||||||
props: TreeNodeProps<RenderProps>
|
props: TreeNodeProps<RenderProps>
|
||||||
) => {
|
) => {
|
||||||
const [{ isDragging }, drag] = useDrag(() => ({
|
const { draggingId, node, allowDrop } = props;
|
||||||
type: 'node',
|
const { attributes, listeners, setNodeRef } = useDraggable({
|
||||||
item: props.node,
|
id: props.node.id,
|
||||||
collect: monitor => ({
|
});
|
||||||
isDragging: monitor.isDragging(),
|
const isDragging = useMemo(
|
||||||
}),
|
() => draggingId === node.id,
|
||||||
}));
|
[draggingId, node.id]
|
||||||
|
);
|
||||||
return <TreeNode dragRef={drag} isDragging={isDragging} {...props} />;
|
return (
|
||||||
|
<StyledTreeNodeContainer
|
||||||
|
ref={setNodeRef}
|
||||||
|
isDragging={isDragging}
|
||||||
|
{...listeners}
|
||||||
|
{...attributes}
|
||||||
|
>
|
||||||
|
<TreeNode
|
||||||
|
{...props}
|
||||||
|
allowDrop={allowDrop === false ? allowDrop : !isDragging}
|
||||||
|
/>
|
||||||
|
</StyledTreeNodeContainer>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TreeNode = <RenderProps,>({
|
export const TreeNode = <RenderProps,>({
|
||||||
node,
|
node,
|
||||||
index,
|
index,
|
||||||
isDragging = false,
|
|
||||||
allowDrop = true,
|
allowDrop = true,
|
||||||
dragRef,
|
|
||||||
...otherProps
|
...otherProps
|
||||||
}: TreeNodeProps<RenderProps>) => {
|
}: TreeNodeProps<RenderProps>) => {
|
||||||
const { indent, enableDnd, collapsedIds } = otherProps;
|
const { indent, enableDnd, collapsedIds } = otherProps;
|
||||||
@@ -149,13 +45,13 @@ export const TreeNode = <RenderProps,>({
|
|||||||
const { renderTopLine = true, renderBottomLine = true } = node;
|
const { renderTopLine = true, renderBottomLine = true } = node;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledTreeNodeContainer ref={dragRef} isDragging={isDragging}>
|
<>
|
||||||
<StyledTreeNodeWrapper>
|
<StyledTreeNodeWrapper>
|
||||||
{enableDnd && renderTopLine && index === 0 && (
|
{enableDnd && renderTopLine && index === 0 && (
|
||||||
<NodeLine
|
<NodeLine
|
||||||
node={node}
|
node={node}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
allowDrop={!isDragging && allowDrop}
|
allowDrop={allowDrop}
|
||||||
isTop={true}
|
isTop={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -180,11 +76,7 @@ export const TreeNode = <RenderProps,>({
|
|||||||
{enableDnd &&
|
{enableDnd &&
|
||||||
renderBottomLine &&
|
renderBottomLine &&
|
||||||
(!node.children?.length || collapsed) && (
|
(!node.children?.length || collapsed) && (
|
||||||
<NodeLine
|
<NodeLine node={node} {...otherProps} allowDrop={allowDrop} />
|
||||||
node={node}
|
|
||||||
{...otherProps}
|
|
||||||
allowDrop={!isDragging && allowDrop}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</StyledTreeNodeWrapper>
|
</StyledTreeNodeWrapper>
|
||||||
<StyledCollapse in={!collapsed} indent={indent}>
|
<StyledCollapse in={!collapsed} indent={indent}>
|
||||||
@@ -195,8 +87,8 @@ export const TreeNode = <RenderProps,>({
|
|||||||
key={childNode.id}
|
key={childNode.id}
|
||||||
node={childNode}
|
node={childNode}
|
||||||
index={index}
|
index={index}
|
||||||
allowDrop={isDragging ? false : allowDrop}
|
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
|
allowDrop={allowDrop}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TreeNode
|
<TreeNode
|
||||||
@@ -209,6 +101,6 @@ export const TreeNode = <RenderProps,>({
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</StyledCollapse>
|
</StyledCollapse>
|
||||||
</StyledTreeNodeContainer>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
92
packages/component/src/ui/tree-view/TreeNodeInner.tsx
Normal file
92
packages/component/src/ui/tree-view/TreeNodeInner.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { useDroppable } from '@dnd-kit/core';
|
||||||
|
|
||||||
|
import { StyledNodeLine } from './styles';
|
||||||
|
import type { NodeLIneProps, TreeNodeItemProps } from './types';
|
||||||
|
|
||||||
|
export const NodeLine = <RenderProps,>({
|
||||||
|
node,
|
||||||
|
allowDrop = true,
|
||||||
|
isTop = false,
|
||||||
|
}: NodeLIneProps<RenderProps>) => {
|
||||||
|
const { isOver, setNodeRef } = useDroppable({
|
||||||
|
id: `${node.id}-${isTop ? 'top' : 'bottom'}-line`,
|
||||||
|
disabled: !allowDrop,
|
||||||
|
data: {
|
||||||
|
node,
|
||||||
|
position: {
|
||||||
|
topLine: isTop,
|
||||||
|
bottomLine: !isTop,
|
||||||
|
internal: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledNodeLine
|
||||||
|
ref={setNodeRef}
|
||||||
|
isOver={isOver && allowDrop}
|
||||||
|
isTop={isTop}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const TreeNodeItemWithDnd = <RenderProps,>({
|
||||||
|
node,
|
||||||
|
allowDrop,
|
||||||
|
setCollapsed,
|
||||||
|
...otherProps
|
||||||
|
}: TreeNodeItemProps<RenderProps>) => {
|
||||||
|
const { onAdd, onDelete } = otherProps;
|
||||||
|
|
||||||
|
const { isOver, setNodeRef } = useDroppable({
|
||||||
|
id: node.id,
|
||||||
|
disabled: !allowDrop,
|
||||||
|
data: {
|
||||||
|
node,
|
||||||
|
position: {
|
||||||
|
topLine: false,
|
||||||
|
bottomLine: false,
|
||||||
|
internal: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={setNodeRef}>
|
||||||
|
<TreeNodeItem
|
||||||
|
onAdd={onAdd}
|
||||||
|
onDelete={onDelete}
|
||||||
|
node={node}
|
||||||
|
allowDrop={allowDrop}
|
||||||
|
setCollapsed={setCollapsed}
|
||||||
|
isOver={isOver}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TreeNodeItem = <RenderProps,>({
|
||||||
|
node,
|
||||||
|
collapsed,
|
||||||
|
setCollapsed,
|
||||||
|
selectedId,
|
||||||
|
isOver = false,
|
||||||
|
onAdd,
|
||||||
|
onDelete,
|
||||||
|
disableCollapse,
|
||||||
|
allowDrop = true,
|
||||||
|
}: TreeNodeItemProps<RenderProps>) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{node.render?.(node, {
|
||||||
|
isOver: isOver && allowDrop,
|
||||||
|
onAdd: () => onAdd?.(node.id),
|
||||||
|
onDelete: () => onDelete?.(node.id),
|
||||||
|
collapsed,
|
||||||
|
setCollapsed,
|
||||||
|
isSelected: selectedId === node.id,
|
||||||
|
disableCollapse,
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,11 +1,20 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import type {
|
||||||
import { DndProvider } from 'react-dnd';
|
DragEndEvent} from '@dnd-kit/core';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import {
|
||||||
|
closestCenter,
|
||||||
|
DndContext,
|
||||||
|
DragOverlay,
|
||||||
|
PointerSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors
|
||||||
|
} from '@dnd-kit/core';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import useCollapsed from './hooks/useCollapsed';
|
||||||
|
import useSelectWithKeyboard from './hooks/useSelectWithKeyboard';
|
||||||
import { TreeNode, TreeNodeWithDnd } from './TreeNode';
|
import { TreeNode, TreeNodeWithDnd } from './TreeNode';
|
||||||
import type { TreeNodeProps, TreeViewProps } from './types';
|
import type { TreeViewProps } from './types';
|
||||||
import { flattenIds } from './utils';
|
import { findNode } from './utils';
|
||||||
|
|
||||||
export const TreeView = <RenderProps,>({
|
export const TreeView = <RenderProps,>({
|
||||||
data,
|
data,
|
||||||
enableKeyboardSelection,
|
enableKeyboardSelection,
|
||||||
@@ -13,69 +22,51 @@ export const TreeView = <RenderProps,>({
|
|||||||
enableDnd = true,
|
enableDnd = true,
|
||||||
initialCollapsedIds = [],
|
initialCollapsedIds = [],
|
||||||
disableCollapse,
|
disableCollapse,
|
||||||
|
onDrop,
|
||||||
...otherProps
|
...otherProps
|
||||||
}: TreeViewProps<RenderProps>) => {
|
}: TreeViewProps<RenderProps>) => {
|
||||||
const [selectedId, setSelectedId] = useState<string>();
|
const sensors = useSensors(
|
||||||
// TODO: should record collapsedIds in localStorage
|
useSensor(PointerSensor, {
|
||||||
const [collapsedIds, setCollapsedIds] =
|
activationConstraint: {
|
||||||
useState<string[]>(initialCollapsedIds);
|
distance: 8,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const { selectedId } = useSelectWithKeyboard({
|
||||||
|
data,
|
||||||
|
onSelect,
|
||||||
|
enableKeyboardSelection,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const { collapsedIds, setCollapsed } = useCollapsed({ disableCollapse });
|
||||||
if (!enableKeyboardSelection) {
|
|
||||||
|
const [draggingId, setDraggingId] = useState<string>();
|
||||||
|
|
||||||
|
const onDragEnd = useCallback(
|
||||||
|
(e: DragEndEvent) => {
|
||||||
|
const { active, over } = e;
|
||||||
|
const position = over?.data.current?.position;
|
||||||
|
const dropId = over?.data.current?.node.id;
|
||||||
|
|
||||||
|
if (!over || !active || !position) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const flattenedIds = flattenIds<RenderProps>(data);
|
onDrop?.(active.id as string, dropId, position);
|
||||||
|
},
|
||||||
const handleDirectionKeyDown = (e: KeyboardEvent) => {
|
[onDrop]
|
||||||
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') {
|
);
|
||||||
return;
|
const onDragMove = useCallback((e: DragEndEvent) => {
|
||||||
}
|
setDraggingId(e.active.id as string);
|
||||||
if (selectedId === undefined) {
|
}, []);
|
||||||
setSelectedId(flattenedIds[0]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let selectedIndex = flattenedIds.indexOf(selectedId);
|
|
||||||
if (e.key === 'ArrowDown') {
|
|
||||||
selectedIndex < flattenedIds.length - 1 && selectedIndex++;
|
|
||||||
}
|
|
||||||
if (e.key === 'ArrowUp') {
|
|
||||||
selectedIndex > 0 && selectedIndex--;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedId(flattenedIds[selectedIndex]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEnterKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if (e.key !== 'Enter') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
selectedId && onSelect?.(selectedId);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('keydown', handleDirectionKeyDown);
|
|
||||||
document.addEventListener('keydown', handleEnterKeyDown);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('keydown', handleDirectionKeyDown);
|
|
||||||
document.removeEventListener('keydown', handleEnterKeyDown);
|
|
||||||
};
|
|
||||||
}, [data, enableKeyboardSelection, onSelect, selectedId]);
|
|
||||||
|
|
||||||
const setCollapsed: TreeNodeProps['setCollapsed'] = (id, collapsed) => {
|
|
||||||
if (disableCollapse) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (collapsed) {
|
|
||||||
setCollapsedIds(ids => [...ids, id]);
|
|
||||||
} else {
|
|
||||||
setCollapsedIds(ids => ids.filter(i => i !== id));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (enableDnd) {
|
if (enableDnd) {
|
||||||
return (
|
return (
|
||||||
<DndProvider backend={HTML5Backend}>
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragMove={onDragMove}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
>
|
||||||
{data.map((node, index) => (
|
{data.map((node, index) => (
|
||||||
<TreeNodeWithDnd
|
<TreeNodeWithDnd
|
||||||
key={node.id}
|
key={node.id}
|
||||||
@@ -86,10 +77,24 @@ export const TreeView = <RenderProps,>({
|
|||||||
selectedId={selectedId}
|
selectedId={selectedId}
|
||||||
enableDnd={enableDnd}
|
enableDnd={enableDnd}
|
||||||
disableCollapse={disableCollapse}
|
disableCollapse={disableCollapse}
|
||||||
|
draggingId={draggingId}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</DndProvider>
|
|
||||||
|
<DragOverlay>
|
||||||
|
{draggingId && (
|
||||||
|
<TreeNode
|
||||||
|
node={findNode(draggingId, data)!}
|
||||||
|
index={0}
|
||||||
|
allowDrop={false}
|
||||||
|
collapsedIds={collapsedIds}
|
||||||
|
setCollapsed={() => {}}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DragOverlay>
|
||||||
|
</DndContext>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
32
packages/component/src/ui/tree-view/hooks/useCollapsed.ts
Normal file
32
packages/component/src/ui/tree-view/hooks/useCollapsed.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import type { TreeNodeProps } from '../types';
|
||||||
|
export const useCollapsed = <RenderProps>({
|
||||||
|
initialCollapsedIds = [],
|
||||||
|
disableCollapse = false,
|
||||||
|
}: {
|
||||||
|
disableCollapse?: boolean;
|
||||||
|
initialCollapsedIds?: string[];
|
||||||
|
}) => {
|
||||||
|
// TODO: should record collapsedIds in localStorage
|
||||||
|
const [collapsedIds, setCollapsedIds] =
|
||||||
|
useState<string[]>(initialCollapsedIds);
|
||||||
|
|
||||||
|
const setCollapsed: TreeNodeProps['setCollapsed'] = (id, collapsed) => {
|
||||||
|
if (disableCollapse) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (collapsed) {
|
||||||
|
setCollapsedIds(ids => [...ids, id]);
|
||||||
|
} else {
|
||||||
|
setCollapsedIds(ids => ids.filter(i => i !== id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
collapsedIds,
|
||||||
|
setCollapsed,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCollapsed;
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import type { TreeViewProps } from '../types';
|
||||||
|
import { flattenIds } from '../utils';
|
||||||
|
export const useSelectWithKeyboard = <RenderProps>({
|
||||||
|
data,
|
||||||
|
enableKeyboardSelection,
|
||||||
|
onSelect,
|
||||||
|
}: Pick<
|
||||||
|
TreeViewProps<RenderProps>,
|
||||||
|
'data' | 'enableKeyboardSelection' | 'onSelect'
|
||||||
|
>) => {
|
||||||
|
const [selectedId, setSelectedId] = useState<string>();
|
||||||
|
// TODO: should record collapsedIds in localStorage
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enableKeyboardSelection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const flattenedIds = flattenIds<RenderProps>(data);
|
||||||
|
|
||||||
|
const handleDirectionKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selectedId === undefined) {
|
||||||
|
setSelectedId(flattenedIds[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let selectedIndex = flattenedIds.indexOf(selectedId);
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
selectedIndex < flattenedIds.length - 1 && selectedIndex++;
|
||||||
|
}
|
||||||
|
if (e.key === 'ArrowUp') {
|
||||||
|
selectedIndex > 0 && selectedIndex--;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedId(flattenedIds[selectedIndex]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnterKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key !== 'Enter') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedId && onSelect?.(selectedId);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleDirectionKeyDown);
|
||||||
|
document.addEventListener('keydown', handleEnterKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleDirectionKeyDown);
|
||||||
|
document.removeEventListener('keydown', handleEnterKeyDown);
|
||||||
|
};
|
||||||
|
}, [data, enableKeyboardSelection, onSelect, selectedId]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedId,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useSelectWithKeyboard;
|
||||||
@@ -16,16 +16,17 @@ export const StyledTreeNodeWrapper = styled('div')(() => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
export const StyledTreeNodeContainer = styled('div')<{ isDragging?: boolean }>(
|
export const StyledTreeNodeContainer = styled('div')<{ isDragging?: boolean }>(
|
||||||
({ isDragging = false, theme }) => {
|
({ isDragging = false }) => {
|
||||||
return {
|
return {
|
||||||
background: isDragging ? 'var(--affine-hover-color)' : '',
|
background: isDragging ? 'var(--affine-hover-color)' : '',
|
||||||
// opacity: isDragging ? 0.4 : 1,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const StyledNodeLine = styled('div')<{ show: boolean; isTop?: boolean }>(
|
export const StyledNodeLine = styled('div')<{
|
||||||
({ show, isTop = false, theme }) => {
|
isOver: boolean;
|
||||||
|
isTop?: boolean;
|
||||||
|
}>(({ isOver, isTop = false }) => {
|
||||||
return {
|
return {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: '0',
|
left: '0',
|
||||||
@@ -33,11 +34,10 @@ export const StyledNodeLine = styled('div')<{ show: boolean; isTop?: boolean }>(
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
paddingTop: '2x',
|
paddingTop: '2x',
|
||||||
borderTop: '2px solid',
|
borderTop: '2px solid',
|
||||||
borderColor: show ? 'var(--affine-primary-color)' : 'transparent',
|
borderColor: isOver ? 'var(--affine-primary-color)' : 'transparent',
|
||||||
boxShadow: show
|
boxShadow: isOver
|
||||||
? `0px 0px 8px ${alpha(lightTheme.primaryColor, 0.35)}`
|
? `0px 0px 8px ${alpha(lightTheme.primaryColor, 0.35)}`
|
||||||
: 'none',
|
: 'none',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
};
|
};
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CSSProperties, ReactNode, Ref } from 'react';
|
import type { CSSProperties, ReactNode } from 'react';
|
||||||
|
|
||||||
export type DropPosition = {
|
export type DropPosition = {
|
||||||
topLine: boolean;
|
topLine: boolean;
|
||||||
@@ -50,8 +50,7 @@ export type TreeNodeProps<RenderProps = unknown> = {
|
|||||||
setCollapsed: (id: string, collapsed: boolean) => void;
|
setCollapsed: (id: string, collapsed: boolean) => void;
|
||||||
allowDrop?: boolean;
|
allowDrop?: boolean;
|
||||||
selectedId?: string;
|
selectedId?: string;
|
||||||
isDragging?: boolean;
|
draggingId?: string;
|
||||||
dragRef?: Ref<HTMLDivElement>;
|
|
||||||
} & CommonProps<RenderProps>;
|
} & CommonProps<RenderProps>;
|
||||||
|
|
||||||
export type TreeNodeItemProps<RenderProps = unknown> = {
|
export type TreeNodeItemProps<RenderProps = unknown> = {
|
||||||
@@ -59,9 +58,6 @@ export type TreeNodeItemProps<RenderProps = unknown> = {
|
|||||||
setCollapsed: (id: string, collapsed: boolean) => void;
|
setCollapsed: (id: string, collapsed: boolean) => void;
|
||||||
|
|
||||||
isOver?: boolean;
|
isOver?: boolean;
|
||||||
canDrop?: boolean;
|
|
||||||
|
|
||||||
dropRef?: Ref<HTMLDivElement>;
|
|
||||||
} & TreeNodeProps<RenderProps>;
|
} & TreeNodeProps<RenderProps>;
|
||||||
|
|
||||||
export type TreeViewProps<RenderProps = unknown> = {
|
export type TreeViewProps<RenderProps = unknown> = {
|
||||||
@@ -73,4 +69,4 @@ export type TreeViewProps<RenderProps = unknown> = {
|
|||||||
export type NodeLIneProps<RenderProps = unknown> = {
|
export type NodeLIneProps<RenderProps = unknown> = {
|
||||||
allowDrop: boolean;
|
allowDrop: boolean;
|
||||||
isTop?: boolean;
|
isTop?: boolean;
|
||||||
} & Pick<TreeNodeProps<RenderProps>, 'node' | 'onDrop'>;
|
} & Pick<TreeNodeProps<RenderProps>, 'node'>;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Node } from '@affine/component';
|
import type { Node } from './types';
|
||||||
|
|
||||||
export function flattenIds<RenderProps>(arr: Node<RenderProps>[]): string[] {
|
export function flattenIds<RenderProps>(arr: Node<RenderProps>[]): string[] {
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
@@ -16,3 +16,21 @@ export function flattenIds<RenderProps>(arr: Node<RenderProps>[]): string[] {
|
|||||||
flatten(arr);
|
flatten(arr);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function findNode<RenderProps>(
|
||||||
|
id: string,
|
||||||
|
nodes: Node<RenderProps>[]
|
||||||
|
): Node<RenderProps> | undefined {
|
||||||
|
for (let i = 0, len = nodes.length; i < len; i++) {
|
||||||
|
const node = nodes[i];
|
||||||
|
if (node.id === id) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
if (node.children) {
|
||||||
|
const result = findNode(id, node.children);
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
13
yarn.lock
13
yarn.lock
@@ -80,8 +80,6 @@ __metadata:
|
|||||||
lit: ^2.7.2
|
lit: ^2.7.2
|
||||||
lottie-web: ^5.11.0
|
lottie-web: ^5.11.0
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
react-dnd: ^16.0.1
|
|
||||||
react-dnd-html5-backend: ^16.0.1
|
|
||||||
react-dom: ^18.2.0
|
react-dom: ^18.2.0
|
||||||
react-error-boundary: ^4.0.3
|
react-error-boundary: ^4.0.3
|
||||||
react-is: ^18.2.0
|
react-is: ^18.2.0
|
||||||
@@ -19643,16 +19641,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-dnd-html5-backend@npm:^16.0.1":
|
"react-dnd@npm:*":
|
||||||
version: 16.0.1
|
|
||||||
resolution: "react-dnd-html5-backend@npm:16.0.1"
|
|
||||||
dependencies:
|
|
||||||
dnd-core: ^16.0.1
|
|
||||||
checksum: e2368bf85d5632a5cd867b743feb54c9052d909ea5331608860fa455edf3c633ac791f5b338e3db29b19ea8670c0ba5fb43c9c1c2510760bea030811d726cdfa
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"react-dnd@npm:*, react-dnd@npm:^16.0.1":
|
|
||||||
version: 16.0.1
|
version: 16.0.1
|
||||||
resolution: "react-dnd@npm:16.0.1"
|
resolution: "react-dnd@npm:16.0.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user