@onehat/ui
Version:
Base UI for OneHat apps
234 lines (211 loc) • 6.31 kB
JavaScript
import { useMemo, useEffect, } from 'react';
import {
Box,
HStackNative,
Icon,
Spinner,
TextNative,
} from '@project-components/Gluestack';
import clsx from 'clsx';
import * as colourMixer from '@k-renwick/colour-mixer';
import {
UI_MODE_WEB,
CURRENT_MODE,
} from '../../Constants/UiModes.js';
import UiGlobals from '../../UiGlobals.js';
import withDraggable from '../Hoc/withDraggable.js';
import IconButton from '../Buttons/IconButton.js';
import { withDragSource, withDropTarget } from '../Hoc/withDnd.js';
import RowHandle from '../Grid/RowHandle.js';
import testProps from '../../Functions/testProps.js';
import ChevronRight from '../Icons/ChevronRight.js';
import ChevronDown from '../Icons/ChevronDown.js';
import _ from 'lodash';
// Conditional import for web only
let getEmptyImage;
if (CURRENT_MODE === UI_MODE_WEB) {
import('react-dnd-html5-backend').then((module) => {
getEmptyImage = module.getEmptyImage;
}).catch(() => {
getEmptyImage = null;
});
}
// This was broken out from Tree simply so we can memoize it
export default function TreeNode(props) {
const {
datum,
nodeProps = {},
onToggle,
bg,
isDraggable,
isDragSource,
isHovered,
isHighlighted,
isOver,
isSelected,
showSelectHandle,
canDrop,
draggedItem,
validateDrop, // same as canDrop (for visual feedback)
getDragProxy,
dragSourceRef,
dragPreviewRef,
dropTargetRef,
...propsToPass
} = props,
styles = UiGlobals.styles,
item = datum.item,
isExpanded = datum.isExpanded,
isLoading = datum.isLoading,
isPhantom = item.isPhantom,
hasChildren = item.hasChildren,
depth = item.depth,
text = datum.text,
content = datum.content,
icon = datum.icon,
hash = item?.hash || item;
// Hide the default drag preview only when using custom drag proxy (and only on web)
useEffect(() => {
if (dragPreviewRef && typeof dragPreviewRef === 'function' && getDragProxy && CURRENT_MODE === UI_MODE_WEB) {
// Only suppress default drag preview when we have a custom one and we're on web
dragPreviewRef(getEmptyImage(), { captureDraggingState: true });
}
}, [dragPreviewRef, getDragProxy]);
return useMemo(() => {
let bg = props.nodeProps?.bg || props.bg || styles.TREE_NODE_BG,
mixWith;
// Determine visual state priority (highest to lowest):
// 1. Drop target states (when being hovered during drag)
// 2. Selection states
// 3. Hover states
// 4. Highlighted state
// Use custom validation for enhanced visual feedback, fallback to React DnD's canDrop
let actualCanDrop = canDrop;
if (isOver && draggedItem && validateDrop) {
actualCanDrop = validateDrop(draggedItem);
}
if (isOver && actualCanDrop) {
// Valid drop target - show positive feedback
mixWith = styles.TREE_NODE_DROP_VALID_BG || '#4ade80'; // green-400 fallback
// } else if (isOver && actualCanDrop === false) {
// // Invalid drop target - show negative feedback
// mixWith = styles.TREE_NODE_DROP_INVALID_BG || '#f87171'; // red-400 fallback
} else if (isSelected) {
if (isHovered) {
mixWith = styles.TREE_NODE_SELECTED_BG_HOVER;
} else {
mixWith = styles.TREE_NODE_SELECTED_BG;
}
} else if (isHovered) {
mixWith = styles.TREE_NODE_BG_HOVER;
} else if (isHighlighted) {
mixWith = styles.TREE_NODE_HIGHLIGHTED_BG;
}
if (mixWith) {
// const
// mixWithObj = gsToHex(mixWith),
// ratio = mixWithObj.alpha ? 1 - mixWithObj.alpha : 0.5;
// bg = colourMixer.blend(bg, ratio, mixWithObj.color);
bg = colourMixer.blend(bg, 0.5, mixWith);
}
let className = clsx(
'TreeNode',
'items-center',
'flex-1',
'grow-1',
'select-none',
'cursor-pointer',
);
// Add drop state classes for additional styling
if (isOver && actualCanDrop) {
className += ' TreeNode--dropValid border-2 border-green-400';
// } else if (isOver && actualCanDrop === false) {
// className += ' TreeNode--dropInvalid border-2 border-red-400';
}
if (props.className) {
className += ' ' + props.className;
}
return <HStackNative
{...testProps('node' + (isSelected ? '-selected' : ''))}
{...nodeProps}
key={hash}
className={className}
style={{
backgroundColor: bg,
}}
ref={(element) => {
if (dropTargetRef && dropTargetRef.current !== undefined) {
// dropTargetRef is a ref object, not a callback
dropTargetRef.current = element;
}
}}
>
{isPhantom && <Box className="absolute t-0 l-0 bg-[#f00] h-[2px] w-[2px]" />}
{(isDragSource || showSelectHandle) &&
<RowHandle
ref={dragSourceRef}
isDragSource={isDragSource}
isDraggable={isDraggable}
showSelectHandle={showSelectHandle}
/>}
{hasChildren && <IconButton
{...testProps('expandBtn')}
icon={isExpanded ? ChevronDown : ChevronRight}
onPress={(e) => onToggle(datum, e)}
className="ml-2"
/>}
{isLoading && <Spinner className="px-2" />}
{!isLoading && icon && <Icon as={icon} className="ml-2 mr-1" />}
{text && <TextNative
numberOfLines={1}
ellipsizeMode="head"
// {...propsToPass}
className={clsx(
'TreeNode-TextNative',
'self-center',
'overflow-hidden',
'flex',
'flex-1',
'text-ellipsis',
styles.TREE_NODE_CLASSNAME,
)}
>{text}</TextNative>}
{content}
</HStackNative>;
}, [
nodeProps,
bg,
item,
hash, // this is an easy way to determine if the data has changed and the item needs to be rerendered
isDragSource,
isExpanded,
isHighlighted,
isLoading,
isOver,
isPhantom,
isSelected,
hasChildren,
depth,
text,
content,
onToggle,
canDrop,
draggedItem,
validateDrop,
dragSourceRef,
dragPreviewRef,
dropTargetRef,
]);
}
function withAdditionalProps(WrappedComponent) {
return (props) => {
return <WrappedComponent
isDraggable={true}
{...props}
/>;
};
}
export const DraggableTreeNode = withAdditionalProps(withDraggable(TreeNode));
export const DragSourceTreeNode = withDragSource(TreeNode);
export const DropTargetTreeNode = withDropTarget(TreeNode);
export const DragSourceDropTargetTreeNode = withDropTarget(withDragSource(TreeNode));