@onehat/ui 0.4.65 → 0.4.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/Components/Fab/DynamicFab.js +16 -10
- package/src/Components/Form/Form.js +32 -27
- package/src/Components/Grid/Grid.js +43 -10
- package/src/Components/Grid/GridHeaderRow.js +3 -3
- package/src/Components/Grid/GridRow.js +35 -4
- package/src/Components/Grid/RowDragHandle.js +1 -1
- package/src/Components/Grid/RowSelectHandle.js +3 -3
- package/src/Components/Hoc/withAlert.js +4 -0
- package/src/Components/Hoc/withDnd.js +54 -57
- package/src/Components/Hoc/withDraggable.js +8 -4
- package/src/Components/Hoc/withEditor.js +7 -1
- package/src/Components/Icons/Arcs.js +10 -0
- package/src/Components/Tree/Tree.js +365 -328
- package/src/Components/Tree/TreeNode.js +103 -29
- package/src/Components/Tree/TreeNodeDragHandle.js +33 -0
- package/src/Components/index.js +2 -0
- package/src/Constants/Styles.js +2 -0
- package/src/PlatformImports/Web/Attachments.js +19 -4
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import Animated, {
|
|
7
7
|
useSharedValue,
|
|
8
8
|
useAnimatedStyle,
|
|
9
|
+
useDerivedValue,
|
|
9
10
|
withTiming,
|
|
10
11
|
} from 'react-native-reanimated';
|
|
11
12
|
import IconButton from '../Buttons/IconButton.js';
|
|
@@ -33,6 +34,15 @@ export default function DynamicFab(props) {
|
|
|
33
34
|
}, []),
|
|
34
35
|
buttonSpacing = 45,
|
|
35
36
|
verticalOffset = 50; // to shift the entire expanded group up
|
|
37
|
+
buttonAnimatedStyle = useAnimatedStyle(() => {
|
|
38
|
+
return {
|
|
39
|
+
opacity: withTiming(isExpanded.value, { duration: 200 }),
|
|
40
|
+
pointerEvents: isExpanded.value ? 'auto' : 'none', // Disable interaction when collapsed
|
|
41
|
+
};
|
|
42
|
+
}),
|
|
43
|
+
isExpandedForRender = useDerivedValue(() => { // Use useDerivedValue to safely read the shared value during render
|
|
44
|
+
return isExpanded.value > 0;
|
|
45
|
+
});
|
|
36
46
|
|
|
37
47
|
let className = `
|
|
38
48
|
DynamicFab
|
|
@@ -55,18 +65,12 @@ export default function DynamicFab(props) {
|
|
|
55
65
|
onPress,
|
|
56
66
|
key,
|
|
57
67
|
...btnConfigToPass
|
|
58
|
-
} = btnConfig
|
|
59
|
-
animatedStyle = useAnimatedStyle(() => {
|
|
60
|
-
return {
|
|
61
|
-
opacity: withTiming(isExpanded.value, { duration: 200 }),
|
|
62
|
-
pointerEvents: isExpanded.value ? 'auto' : 'none', // Disable interaction when collapsed
|
|
63
|
-
};
|
|
64
|
-
});
|
|
68
|
+
} = btnConfig;
|
|
65
69
|
|
|
66
70
|
return <Animated.View
|
|
67
71
|
key={ix}
|
|
68
72
|
style={[
|
|
69
|
-
|
|
73
|
+
buttonAnimatedStyle,
|
|
70
74
|
{
|
|
71
75
|
position: 'absolute',
|
|
72
76
|
bottom: buttonSpacing * (ix + 1) + verticalOffset, // Static vertical positioning
|
|
@@ -101,8 +105,10 @@ export default function DynamicFab(props) {
|
|
|
101
105
|
tooltipClassName={tooltipClassName}
|
|
102
106
|
tooltipTriggerClassName={tooltipTriggerClassName}
|
|
103
107
|
>
|
|
104
|
-
<
|
|
108
|
+
<Animated.View>
|
|
109
|
+
<FabIcon as={isExpandedForRender.value ? Xmark : icon || EllipsisVertical} />
|
|
110
|
+
</Animated.View>
|
|
105
111
|
{label ? <FabLabel>{label}</FabLabel> : null}
|
|
106
112
|
</FabWithTooltip>
|
|
107
113
|
</VStack>;
|
|
108
|
-
}
|
|
114
|
+
}
|
|
@@ -162,9 +162,9 @@ function Form(props) {
|
|
|
162
162
|
let skipAll = false;
|
|
163
163
|
if (record?.isDestroyed) {
|
|
164
164
|
skipAll = true; // if record is destroyed, skip render, but allow hooks to still be called
|
|
165
|
-
if (self?.parent?.parent?.setIsEditorShown) {
|
|
166
|
-
|
|
167
|
-
}
|
|
165
|
+
// if (self?.parent?.parent?.setIsEditorShown) {
|
|
166
|
+
// self.parent.parent.setIsEditorShown(false); // close the editor
|
|
167
|
+
// }
|
|
168
168
|
}
|
|
169
169
|
const
|
|
170
170
|
isMultiple = _.isArray(record),
|
|
@@ -363,7 +363,7 @@ function Form(props) {
|
|
|
363
363
|
style.width = boxW;
|
|
364
364
|
}
|
|
365
365
|
elements.push(<Box
|
|
366
|
-
key={ix}
|
|
366
|
+
key={fieldName + '-' + ix}
|
|
367
367
|
className={columnClassName}
|
|
368
368
|
style={style}
|
|
369
369
|
>{element}</Box>);
|
|
@@ -467,7 +467,7 @@ function Form(props) {
|
|
|
467
467
|
`}
|
|
468
468
|
/> : null;
|
|
469
469
|
return <HStack
|
|
470
|
-
key={ix}
|
|
470
|
+
key={fieldName + '-HStack-' + ix}
|
|
471
471
|
className={`
|
|
472
472
|
Form-HStack1
|
|
473
473
|
flex-${flex}
|
|
@@ -624,7 +624,7 @@ function Form(props) {
|
|
|
624
624
|
itemDefaultsToPass = itemDefaults;
|
|
625
625
|
}
|
|
626
626
|
return <Element
|
|
627
|
-
key={ix}
|
|
627
|
+
key={'column-Element-' + type + '-' + ix}
|
|
628
628
|
title={title}
|
|
629
629
|
{...defaultsToPass}
|
|
630
630
|
{...itemDefaultsToPass}
|
|
@@ -697,7 +697,7 @@ function Form(props) {
|
|
|
697
697
|
</VStack>;
|
|
698
698
|
}
|
|
699
699
|
}
|
|
700
|
-
return <HStack key={ix} className="Form-HStack3 w-full px-2 pb-1">{element}</HStack>;
|
|
700
|
+
return <HStack key={'Form-HStack3-' + ix} className="Form-HStack3 w-full px-2 pb-1">{element}</HStack>;
|
|
701
701
|
}
|
|
702
702
|
|
|
703
703
|
|
|
@@ -908,7 +908,7 @@ function Form(props) {
|
|
|
908
908
|
`}
|
|
909
909
|
/> : null;
|
|
910
910
|
return <HStack
|
|
911
|
-
key={ix}
|
|
911
|
+
key={'Controller-HStack-' + ix}
|
|
912
912
|
className={`
|
|
913
913
|
Form-HStack11
|
|
914
914
|
min-h-[50px]
|
|
@@ -930,6 +930,7 @@ function Form(props) {
|
|
|
930
930
|
|
|
931
931
|
// add the "scroll to top" button
|
|
932
932
|
getAncillaryButtons().push({
|
|
933
|
+
key: 'scrollToTop',
|
|
933
934
|
icon: ArrowUp,
|
|
934
935
|
reference: 'scrollToTop',
|
|
935
936
|
onPress: () => scrollToAncillaryItem(0),
|
|
@@ -938,14 +939,15 @@ function Form(props) {
|
|
|
938
939
|
|
|
939
940
|
_.each(ancillaryItems, (item, ix) => {
|
|
940
941
|
let {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
942
|
+
type,
|
|
943
|
+
title = null,
|
|
944
|
+
description = null,
|
|
945
|
+
icon,
|
|
946
|
+
selectorId,
|
|
947
|
+
selectorSelectedField,
|
|
948
|
+
...itemPropsToPass
|
|
949
|
+
} = item,
|
|
950
|
+
titleElement;
|
|
949
951
|
if (isMultiple && type !== 'Attachments') {
|
|
950
952
|
return;
|
|
951
953
|
}
|
|
@@ -953,6 +955,7 @@ function Form(props) {
|
|
|
953
955
|
// NOTE: this assumes that if one Ancillary item has an icon, they all do.
|
|
954
956
|
// If they don't, the ix will be wrong!
|
|
955
957
|
getAncillaryButtons().push({
|
|
958
|
+
key: 'ancillaryBtn-' + ix,
|
|
956
959
|
icon,
|
|
957
960
|
onPress: () => scrollToAncillaryItem(ix +1), // offset for the "scroll to top" button
|
|
958
961
|
tooltip: title,
|
|
@@ -977,15 +980,15 @@ function Form(props) {
|
|
|
977
980
|
if (record?.displayValue) {
|
|
978
981
|
title += ' for ' + record.displayValue;
|
|
979
982
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
983
|
+
titleElement = <Text
|
|
984
|
+
className={`
|
|
985
|
+
Form-Ancillary-Title
|
|
986
|
+
font-bold
|
|
987
|
+
${styles.FORM_ANCILLARY_TITLE_CLASSNAME}
|
|
988
|
+
`}
|
|
989
|
+
>{title}</Text>;
|
|
987
990
|
if (icon) {
|
|
988
|
-
|
|
991
|
+
titleElement = <HStack className="items-center"><Icon as={icon} className="w-[32px] h-[32px] mr-2" />{titleElement}</HStack>
|
|
989
992
|
}
|
|
990
993
|
}
|
|
991
994
|
if (description) {
|
|
@@ -1006,7 +1009,7 @@ function Form(props) {
|
|
|
1006
1009
|
my-3
|
|
1007
1010
|
`}
|
|
1008
1011
|
>
|
|
1009
|
-
{
|
|
1012
|
+
{titleElement}
|
|
1010
1013
|
{description}
|
|
1011
1014
|
{element}
|
|
1012
1015
|
</VStack>);
|
|
@@ -1369,13 +1372,15 @@ function Form(props) {
|
|
|
1369
1372
|
text={submitBtnLabel || 'Submit'}
|
|
1370
1373
|
/>}
|
|
1371
1374
|
|
|
1372
|
-
{additionalFooterButtons && _.map(additionalFooterButtons, (props) => {
|
|
1375
|
+
{additionalFooterButtons && _.map(additionalFooterButtons, (props, ix) => {
|
|
1373
1376
|
let isDisabled = false;
|
|
1374
1377
|
if (props.disableOnInvalid) {
|
|
1375
1378
|
isDisabled = !formState.isValid;
|
|
1376
1379
|
}
|
|
1380
|
+
const key = 'additionalFooterBtn-' + ix;
|
|
1377
1381
|
return <Button
|
|
1378
|
-
{...testProps(
|
|
1382
|
+
{...testProps(key)}
|
|
1383
|
+
key={key}
|
|
1379
1384
|
{...props}
|
|
1380
1385
|
onPress={(e) => handleSubmit(props.onPress, onSubmitError)(e)}
|
|
1381
1386
|
icon={props.icon || null}
|
|
@@ -133,16 +133,11 @@ function GridComponent(props) {
|
|
|
133
133
|
showHeaders = true,
|
|
134
134
|
showHovers = true,
|
|
135
135
|
showSelectHandle = true,
|
|
136
|
+
isRowSelectable = true,
|
|
137
|
+
isRowHoverable = true,
|
|
136
138
|
canColumnsSort = true,
|
|
137
139
|
canColumnsReorder = true,
|
|
138
140
|
canColumnsResize = true,
|
|
139
|
-
canRowsReorder = false,
|
|
140
|
-
areRowsDragSource = false,
|
|
141
|
-
rowDragSourceType,
|
|
142
|
-
getRowDragSourceItem,
|
|
143
|
-
areRowsDropTarget = false,
|
|
144
|
-
dropTargetAccept,
|
|
145
|
-
onRowDrop,
|
|
146
141
|
allowToggleSelection = false, // i.e. single click with no shift key toggles the selection of the item clicked on
|
|
147
142
|
disableBottomToolbar = false,
|
|
148
143
|
disablePagination = false,
|
|
@@ -173,6 +168,18 @@ function GridComponent(props) {
|
|
|
173
168
|
noSelectorMeansNoResults = false,
|
|
174
169
|
disableSelectorSelected = false,
|
|
175
170
|
|
|
171
|
+
// DND
|
|
172
|
+
canRowsReorder = false,
|
|
173
|
+
canRowAcceptDrop, // optional fn to customize whether each node can accept a dropped item: (targetItem, draggedItem) => boolean
|
|
174
|
+
getCustomDragProxy, // optional fn to render custom drag preview: (item, selection) => ReactElement
|
|
175
|
+
dragPreviewOptions, // optional object for drag preview positioning options
|
|
176
|
+
areRowsDragSource = false,
|
|
177
|
+
rowDragSourceType,
|
|
178
|
+
getRowDragSourceItem,
|
|
179
|
+
areRowsDropTarget = false,
|
|
180
|
+
dropTargetAccept,
|
|
181
|
+
onRowDrop,
|
|
182
|
+
|
|
176
183
|
// withComponent
|
|
177
184
|
self,
|
|
178
185
|
|
|
@@ -473,7 +480,13 @@ function GridComponent(props) {
|
|
|
473
480
|
onContextMenu(item, e, newSelection);
|
|
474
481
|
}
|
|
475
482
|
}}
|
|
476
|
-
className=
|
|
483
|
+
className={`
|
|
484
|
+
Pressable
|
|
485
|
+
Row
|
|
486
|
+
flex-row
|
|
487
|
+
grow
|
|
488
|
+
`}
|
|
489
|
+
>
|
|
477
490
|
{({
|
|
478
491
|
hovered,
|
|
479
492
|
focused,
|
|
@@ -505,12 +518,11 @@ function GridComponent(props) {
|
|
|
505
518
|
}
|
|
506
519
|
return headerRow;
|
|
507
520
|
}
|
|
508
|
-
|
|
509
521
|
const
|
|
510
522
|
rowReorderProps = {},
|
|
511
523
|
rowDragProps = {};
|
|
512
524
|
let WhichRow = GridRow;
|
|
513
|
-
if (CURRENT_MODE === UI_MODE_WEB) { // DND is
|
|
525
|
+
if (CURRENT_MODE === UI_MODE_WEB) { // DND is currently web-only TODO: implement for RN
|
|
514
526
|
// Create a method that gets an always-current copy of the selection ids
|
|
515
527
|
dragSelectionRef.current = selection;
|
|
516
528
|
const getSelection = () => dragSelectionRef.current;
|
|
@@ -542,10 +554,21 @@ function GridComponent(props) {
|
|
|
542
554
|
} else {
|
|
543
555
|
rowDragProps.dragSourceItem = {
|
|
544
556
|
id: item.id,
|
|
557
|
+
item,
|
|
545
558
|
getSelection,
|
|
546
559
|
type: rowDragSourceType,
|
|
547
560
|
};
|
|
548
561
|
}
|
|
562
|
+
|
|
563
|
+
// Add custom drag preview options
|
|
564
|
+
if (dragPreviewOptions) {
|
|
565
|
+
rowDragProps.dragPreviewOptions = dragPreviewOptions;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Add drag preview rendering
|
|
569
|
+
rowDragProps.getDragProxy = getCustomDragProxy ?
|
|
570
|
+
(dragItem) => getCustomDragProxy(item, getSelection()) :
|
|
571
|
+
null; // Let GlobalDragProxy handle the default case
|
|
549
572
|
}
|
|
550
573
|
if (areRowsDropTarget) {
|
|
551
574
|
WhichRow = DropTargetGridRow;
|
|
@@ -555,6 +578,14 @@ function GridComponent(props) {
|
|
|
555
578
|
// NOTE: item is sometimes getting destroyed, but it still as the id, so you can still use it
|
|
556
579
|
onRowDrop(item, droppedItem); // item is what it was dropped on; droppedItem is the dragSourceItem defined above
|
|
557
580
|
};
|
|
581
|
+
rowDragProps.canDrop = (droppedItem, monitor) => {
|
|
582
|
+
// Check if the drop operation would be valid based on business rules
|
|
583
|
+
if (canRowAcceptDrop && typeof canRowAcceptDrop === 'function') {
|
|
584
|
+
return canRowAcceptDrop(item, droppedItem);
|
|
585
|
+
}
|
|
586
|
+
// Default: allow all drops
|
|
587
|
+
return true;
|
|
588
|
+
};
|
|
558
589
|
}
|
|
559
590
|
if (areRowsDragSource && areRowsDropTarget) {
|
|
560
591
|
WhichRow = DragSourceDropTargetGridRow;
|
|
@@ -569,6 +600,8 @@ function GridComponent(props) {
|
|
|
569
600
|
fields={fields}
|
|
570
601
|
rowProps={rowProps}
|
|
571
602
|
hideNavColumn={hideNavColumn}
|
|
603
|
+
isRowSelectable={isRowSelectable}
|
|
604
|
+
isRowHoverable={isRowHoverable}
|
|
572
605
|
isSelected={isSelected}
|
|
573
606
|
isHovered={hovered}
|
|
574
607
|
showHovers={showHovers}
|
|
@@ -20,7 +20,7 @@ import UiGlobals from '../../UiGlobals.js';
|
|
|
20
20
|
import useBlocking from '../../Hooks/useBlocking.js';
|
|
21
21
|
import testProps from '../../Functions/testProps.js';
|
|
22
22
|
import AngleRight from '../Icons/AngleRight.js';
|
|
23
|
-
import
|
|
23
|
+
import Arcs from '../Icons/Arcs.js';
|
|
24
24
|
import HeaderReorderHandle from './HeaderReorderHandle.js';
|
|
25
25
|
import HeaderResizeHandle from './HeaderResizeHandle.js';
|
|
26
26
|
import HeaderColumnSelectorHandle from './HeaderColumnSelectorHandle.js';
|
|
@@ -469,13 +469,13 @@ export default function GridHeaderRow(props) {
|
|
|
469
469
|
key="RowSelectHandle"
|
|
470
470
|
className="Spacer-RowSelectHandle px-2 items-center justify-center flex-none w-[40px]"
|
|
471
471
|
>
|
|
472
|
-
<Icon as={
|
|
472
|
+
<Icon as={Arcs} className={`Arcs w-[20px] h-[20px] text-[#aaa]`} />
|
|
473
473
|
</Box>);
|
|
474
474
|
}
|
|
475
475
|
if (areRowsDragSource) {
|
|
476
476
|
headerColumns.unshift(<Box
|
|
477
477
|
key="spacer"
|
|
478
|
-
className="Spacer w-[
|
|
478
|
+
className="Spacer w-[7px]"
|
|
479
479
|
/>);
|
|
480
480
|
}
|
|
481
481
|
if (!hideNavColumn) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo, } from 'react';
|
|
1
|
+
import { useMemo, useEffect, } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
Box,
|
|
4
4
|
HStack,
|
|
@@ -29,6 +29,8 @@ function GridRow(props) {
|
|
|
29
29
|
rowProps,
|
|
30
30
|
hideNavColumn,
|
|
31
31
|
showSelectHandle,
|
|
32
|
+
isRowSelectable,
|
|
33
|
+
isRowHoverable,
|
|
32
34
|
isSelected,
|
|
33
35
|
isHovered,
|
|
34
36
|
bg,
|
|
@@ -41,14 +43,28 @@ function GridRow(props) {
|
|
|
41
43
|
isDraggable = false, // withDraggable
|
|
42
44
|
isDragSource = false, // withDnd
|
|
43
45
|
isOver = false, // drop target
|
|
46
|
+
canDrop,
|
|
47
|
+
draggedItem,
|
|
48
|
+
validateDrop, // same as canDrop (for visual feedback)
|
|
49
|
+
getDragProxy,
|
|
44
50
|
dragSourceRef,
|
|
51
|
+
dragPreviewRef,
|
|
45
52
|
dropTargetRef,
|
|
53
|
+
...propsToPass
|
|
46
54
|
} = props,
|
|
47
55
|
styles = UiGlobals.styles;
|
|
48
56
|
|
|
49
57
|
if (item.isDestroyed) {
|
|
50
58
|
return null;
|
|
51
59
|
}
|
|
60
|
+
|
|
61
|
+
// Hide the default drag preview only when using custom drag proxy (and only on web)
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (dragPreviewRef && typeof dragPreviewRef === 'function' && getDragProxy && CURRENT_MODE === UI_MODE_WEB) {
|
|
64
|
+
// Only suppress default drag preview when we have a custom one and we're on web
|
|
65
|
+
dragPreviewRef(getEmptyImage(), { captureDraggingState: true });
|
|
66
|
+
}
|
|
67
|
+
}, [dragPreviewRef, getDragProxy]);
|
|
52
68
|
|
|
53
69
|
const
|
|
54
70
|
isPhantom = item.isPhantom,
|
|
@@ -57,13 +73,22 @@ function GridRow(props) {
|
|
|
57
73
|
|
|
58
74
|
let bg = rowProps.bg || props.bg || styles.GRID_ROW_BG,
|
|
59
75
|
mixWith;
|
|
60
|
-
|
|
76
|
+
|
|
77
|
+
// TODO: Finish Drop styling
|
|
78
|
+
|
|
79
|
+
// Use custom validation for enhanced visual feedback, fallback to React DnD's canDrop
|
|
80
|
+
let actualCanDrop = canDrop;
|
|
81
|
+
if (isOver && draggedItem && validateDrop) {
|
|
82
|
+
actualCanDrop = validateDrop(draggedItem);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (isRowSelectable && isSelected) {
|
|
61
86
|
if (showHovers && isHovered) {
|
|
62
87
|
mixWith = styles.GRID_ROW_SELECTED_BG_HOVER;
|
|
63
88
|
} else {
|
|
64
89
|
mixWith = styles.GRID_ROW_SELECTED_BG;
|
|
65
90
|
}
|
|
66
|
-
} else if (showHovers && isHovered) {
|
|
91
|
+
} else if (isRowHoverable && showHovers && isHovered) {
|
|
67
92
|
mixWith = styles.GRID_ROW_BG_HOVER;
|
|
68
93
|
} else if (alternateRowBackgrounds && index % alternatingInterval === 0) { // i.e. every second line, or every third line
|
|
69
94
|
mixWith = styles.GRID_ROW_ALTERNATE_BG;
|
|
@@ -87,7 +112,8 @@ function GridRow(props) {
|
|
|
87
112
|
}
|
|
88
113
|
const
|
|
89
114
|
propsToPass = columnProps[key] || {},
|
|
90
|
-
colStyle = {}
|
|
115
|
+
colStyle = {},
|
|
116
|
+
whichCursor = showSelectHandle ? 'cursor-text' : 'cursor-pointer'; // when using rowSelectHandle, indicate that the row text is selectable, otherwise indicate that the row itself is selectable
|
|
91
117
|
let colClassName = `
|
|
92
118
|
GridRow-column
|
|
93
119
|
p-1
|
|
@@ -95,6 +121,7 @@ function GridRow(props) {
|
|
|
95
121
|
border-r-black-100
|
|
96
122
|
block
|
|
97
123
|
overflow-auto
|
|
124
|
+
${whichCursor}
|
|
98
125
|
${styles.GRID_ROW_MAX_HEIGHT_EXTRA}
|
|
99
126
|
`;
|
|
100
127
|
if (isOnlyOneVisibleColumn) {
|
|
@@ -364,7 +391,11 @@ function GridRow(props) {
|
|
|
364
391
|
isHovered,
|
|
365
392
|
isOver,
|
|
366
393
|
index,
|
|
394
|
+
canDrop,
|
|
395
|
+
draggedItem,
|
|
396
|
+
validateDrop,
|
|
367
397
|
dragSourceRef,
|
|
398
|
+
dragPreviewRef,
|
|
368
399
|
dropTargetRef,
|
|
369
400
|
]);
|
|
370
401
|
}
|
|
@@ -7,7 +7,7 @@ import GripVertical from '../Icons/GripVertical.js';
|
|
|
7
7
|
|
|
8
8
|
function RowDragHandle(props) { return <VStack
|
|
9
9
|
style={styles.ewResize}
|
|
10
|
-
className="RowDragHandle bg-grey-100 w-[
|
|
10
|
+
className="RowDragHandle bg-grey-100 w-[7px] items-center justify-center select-none"
|
|
11
11
|
>
|
|
12
12
|
<Icon
|
|
13
13
|
as={GripVertical}
|
|
@@ -2,14 +2,14 @@ import {
|
|
|
2
2
|
Icon,
|
|
3
3
|
VStack,
|
|
4
4
|
} from '@project-components/Gluestack';
|
|
5
|
-
import
|
|
5
|
+
import Arcs from '../Icons/Arcs.js';
|
|
6
6
|
|
|
7
7
|
function RowSelectHandle(props) {
|
|
8
8
|
return <VStack
|
|
9
|
-
className="RowSelectHandle w-[40px] px-2 items-center justify-center select-none cursor-
|
|
9
|
+
className="RowSelectHandle w-[40px] px-2 items-center justify-center select-none cursor-pointer"
|
|
10
10
|
>
|
|
11
11
|
<Icon
|
|
12
|
-
as={
|
|
12
|
+
as={Arcs}
|
|
13
13
|
size="xs"
|
|
14
14
|
className="w-[20px] h-[20px] text-[#ddd]" />
|
|
15
15
|
</VStack>;
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { forwardRef, useEffect, useRef, } from 'react';
|
|
2
2
|
import { useDrag, useDrop, useDragLayer } from 'react-dnd'; // https://react-dnd.github.io/react-dnd/about don't forget the wrapping <DndProvider /> as shown here: https://react-dnd.github.io/react-dnd/docs/api/dnd-provider
|
|
3
|
-
|
|
3
|
+
import {
|
|
4
|
+
Box,
|
|
5
|
+
} from '@project-components/Gluestack';
|
|
6
|
+
import {
|
|
7
|
+
UI_MODE_WEB,
|
|
8
|
+
UI_MODE_NATIVE,
|
|
9
|
+
CURRENT_MODE,
|
|
10
|
+
} from '../../Constants/UiModes.js';
|
|
4
11
|
|
|
5
12
|
// This HOC allows components to be dragged and dropped onto another component.
|
|
6
13
|
// It can't contrain the movement of the preview item, because react-dnd uses
|
|
@@ -32,6 +39,7 @@ export function withDragSource(WrappedComponent) {
|
|
|
32
39
|
onDragEnd = null,
|
|
33
40
|
canDrag = null,
|
|
34
41
|
isDragging = null,
|
|
42
|
+
getDragProxy,
|
|
35
43
|
dragCollect = (monitor, props2) => { // Optional. The collecting function. It should return a plain object of the props to return for injection into your component. It receives two parameters, monitor and props. Read the overview for an introduction to the monitors and the collecting function. See the collecting function described in detail in the next section.
|
|
36
44
|
// monitor fn determines which props from dnd state get passed
|
|
37
45
|
return {
|
|
@@ -54,7 +62,10 @@ export function withDragSource(WrappedComponent) {
|
|
|
54
62
|
|
|
55
63
|
return {
|
|
56
64
|
type: dragSourceType, // Required. This must be either a string or a symbol. Only the drop targets registered for the same type will react to this item.
|
|
57
|
-
item:
|
|
65
|
+
item: {
|
|
66
|
+
...dragSourceItem,
|
|
67
|
+
getDragProxy,
|
|
68
|
+
}, // Required (object or function).
|
|
58
69
|
// When an object, it is a plain JavaScript object describing the data being dragged. This is the only information available to the drop targets about the drag source so it's important to pick the minimal data they need to know. You may be tempted to put a complex reference here, but you should try very hard to avoid doing this because it couples the drag sources and drop targets. It's a good idea to use something like { id }.
|
|
59
70
|
// When a function, it is fired at the beginning of the drag operation and returns an object representing the drag operation (see first bullet). If null is returned, the drag operation is cancelled.
|
|
60
71
|
previewOptions: dragPreviewOptions, // Optional. A plain JavaScript object describing drag preview options.
|
|
@@ -139,6 +150,7 @@ export function withDropTarget(WrappedComponent) {
|
|
|
139
150
|
return {
|
|
140
151
|
canDrop: !!monitor.canDrop(),
|
|
141
152
|
isOver: !!monitor.isOver(),
|
|
153
|
+
draggedItem: monitor.getItem(), // Pass the dragged item so TreeNode can evaluate custom logic
|
|
142
154
|
};
|
|
143
155
|
},
|
|
144
156
|
} = props,
|
|
@@ -171,6 +183,7 @@ export function withDropTarget(WrappedComponent) {
|
|
|
171
183
|
{
|
|
172
184
|
canDrop: stateCanDrop,
|
|
173
185
|
isOver,
|
|
186
|
+
draggedItem,
|
|
174
187
|
// didDrop,
|
|
175
188
|
// clientOffset,
|
|
176
189
|
// differenceFromInitialOffset,
|
|
@@ -187,64 +200,48 @@ export function withDropTarget(WrappedComponent) {
|
|
|
187
200
|
ref={ref}
|
|
188
201
|
isOver={isOver}
|
|
189
202
|
dropTargetRef={localTargetRef}
|
|
203
|
+
draggedItem={draggedItem} // Pass the dragged item
|
|
190
204
|
{...props}
|
|
191
205
|
/>;
|
|
192
206
|
});
|
|
193
207
|
}
|
|
194
208
|
|
|
195
209
|
|
|
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
|
-
// return (
|
|
236
|
-
// <div id="dragLayer" style={{
|
|
237
|
-
// background: '#f00',
|
|
238
|
-
// position: 'fixed',
|
|
239
|
-
// pointerEvents: 'none',
|
|
240
|
-
// zIndex: 10000,
|
|
241
|
-
// width: '200px',
|
|
242
|
-
// height: '10px',
|
|
243
|
-
// left: 0,
|
|
244
|
-
// top: 0,
|
|
245
|
-
// transform,
|
|
246
|
-
// }}>
|
|
247
|
-
// {children}
|
|
248
|
-
// </div>
|
|
249
|
-
// );
|
|
250
|
-
// }
|
|
210
|
+
export function GlobalDragProxy() {
|
|
211
|
+
const {
|
|
212
|
+
isDragging,
|
|
213
|
+
item,
|
|
214
|
+
currentOffset,
|
|
215
|
+
} = useDragLayer((monitor) => ({
|
|
216
|
+
isDragging: monitor.isDragging(),
|
|
217
|
+
item: monitor.getItem(),
|
|
218
|
+
currentOffset: monitor.getSourceClientOffset(),
|
|
219
|
+
}));
|
|
220
|
+
|
|
221
|
+
if (!isDragging || !currentOffset || CURRENT_MODE !== UI_MODE_WEB) { // Native uses a native drag layer, so we don't need to render a custom proxy
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const getDragProxy = item?.getDragProxy;
|
|
226
|
+
if (!getDragProxy) {
|
|
227
|
+
// Only render a custom proxy if one is provided - let React DnD handle default case
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let proxyContent = null;
|
|
232
|
+
try {
|
|
233
|
+
proxyContent = getDragProxy(item);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.warn('Failed to render custom drag proxy:', error);
|
|
236
|
+
return null; // use default React DnD proxy
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return <Box
|
|
240
|
+
className="fixed pointer-events-none z-[10000] left-0 top-0"
|
|
241
|
+
style={{
|
|
242
|
+
transform: `translate(${currentOffset.x}px, ${currentOffset.y}px)`,
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
{proxyContent}
|
|
246
|
+
</Box>;
|
|
247
|
+
}
|