Skip to content

Commit dc3f94b

Browse files
committed
feat: reparenting working poc (#148)
1 parent 16134f6 commit dc3f94b

File tree

3 files changed

+180
-68
lines changed

3 files changed

+180
-68
lines changed

packages/core/src/controlledEnvironment/DraggingPositionEvaluation.ts

+154-68
Original file line numberDiff line numberDiff line change
@@ -43,29 +43,21 @@ export class DraggingPositionEvaluation {
4343
this.targetItem = this.env.linearItems[this.treeId][this.linearIndex];
4444
}
4545

46-
getDraggingPosition(): DraggingPosition | undefined {
47-
if (!this.evaluator.draggingItems) {
48-
return undefined;
49-
}
50-
51-
if (this.env.linearItems[this.treeId].length === 0) {
52-
// Empty tree
53-
return {
54-
targetType: 'root',
55-
treeId: this.treeId,
56-
depth: 0,
57-
linearIndex: 0,
58-
targetItem: this.env.trees[this.treeId].rootItem,
59-
};
60-
}
61-
62-
if (
63-
this.linearIndex < 0 ||
64-
this.linearIndex >= this.env.linearItems[this.treeId].length
65-
) {
66-
return undefined;
67-
}
46+
private getEmptyTreeDragPosition(): DraggingPosition {
47+
return {
48+
targetType: 'root',
49+
treeId: this.treeId,
50+
depth: 0,
51+
linearIndex: 0,
52+
targetItem: this.env.trees[this.treeId].rootItem,
53+
};
54+
}
6855

56+
/**
57+
* If reordering is not allowed, dragging on non-folder items redirects
58+
* the drag target to the parent of the target item.
59+
*/
60+
private maybeRedirectToParent() {
6961
const redirectTargetToParent =
7062
!this.env.canReorderItems &&
7163
!this.env.canDropOnNonFolder &&
@@ -77,17 +69,13 @@ export class DraggingPositionEvaluation {
7769
this.targetItem = parent;
7870
this.linearIndex = parentLinearIndex;
7971
}
72+
}
8073

81-
if (
82-
this.isDescendant(
83-
this.treeId,
84-
this.linearIndex,
85-
this.evaluator.draggingItems
86-
)
87-
) {
88-
return undefined;
89-
}
90-
74+
/**
75+
* If the item is the last in a group, and the drop is at the bottom,
76+
* the x-coordinate of the mouse allows to reparent upwards.
77+
*/
78+
private maybeReparentUpwards(): DraggingPosition | undefined {
9179
const treeLinearItems = this.env.linearItems[this.treeId];
9280
const deepestDepth = treeLinearItems[this.linearIndex].depth;
9381
const legalDropDepthCount = // itemDepthDifferenceToNextItem/isLastInGroup
@@ -96,84 +84,171 @@ export class DraggingPositionEvaluation {
9684
this.offset === 'bottom' && legalDropDepthCount > 0;
9785
// Default to zero on last position to allow dropping on root when
9886
// dropping at bottom
99-
if (canReparentUpwards) {
100-
const droppingIndent = Math.max(
101-
deepestDepth - legalDropDepthCount,
102-
this.indentation
87+
88+
if (!canReparentUpwards) {
89+
return undefined;
90+
}
91+
92+
const droppingIndent = Math.max(
93+
deepestDepth - legalDropDepthCount,
94+
this.indentation
95+
);
96+
97+
let newParent = {
98+
parentLinearIndex: this.linearIndex,
99+
parent: this.targetItem,
100+
};
101+
let insertionItemAbove: typeof newParent | undefined;
102+
103+
for (let i = deepestDepth; i >= droppingIndent; i -= 1) {
104+
insertionItemAbove = newParent;
105+
newParent = this.evaluator.getParentOfLinearItem(
106+
newParent.parentLinearIndex,
107+
this.treeId
103108
);
104-
let newParent = {
105-
parentLinearIndex: this.linearIndex,
106-
parent: this.targetItem,
107-
};
108-
for (let i = deepestDepth; i !== droppingIndent; i -= 1) {
109-
newParent = this.evaluator.getParentOfLinearItem(
110-
newParent.parentLinearIndex,
111-
this.treeId
112-
);
113-
}
109+
}
114110

115-
if (this.indentation !== treeLinearItems[this.linearIndex].depth) {
116-
this.targetItem = newParent.parent;
117-
}
111+
if (this.indentation === treeLinearItems[this.linearIndex].depth) {
112+
return undefined;
118113
}
114+
if (!insertionItemAbove) {
115+
return undefined;
116+
}
117+
118+
// this.targetItem = newParent.parent;
119+
const reparentedChildIndex =
120+
this.env.items[newParent.parent.item].children!.indexOf(
121+
insertionItemAbove.parent.item
122+
) + 1;
123+
console.log(
124+
`new parent is ${newParent.parent.item}, target is ${this.targetItem.item} and reparentedChildIndex is ${reparentedChildIndex}}`
125+
);
126+
console.log(newParent);
127+
console.log('new depth is', newParent.parent.depth + 1);
119128

129+
return {
130+
targetType: 'between-items',
131+
treeId: this.treeId,
132+
// parentItem: this.evaluator.getParentOfLinearItem(
133+
// newParent.parentLinearIndex,
134+
// this.treeId
135+
// ).parent.item,
136+
parentItem: newParent.parent.item,
137+
// depth: newParent.parent.depth + 1,
138+
depth: droppingIndent,
139+
linearIndex: this.linearIndex + 1,
140+
childIndex: reparentedChildIndex,
141+
linePosition: 'bottom',
142+
} as const;
143+
}
144+
145+
/**
146+
* Don't allow to drop at bottom of an open folder, since that will place
147+
* it visually at a different position. Redirect the drag target to the
148+
* top of the folder contents in that case.
149+
*/
150+
private maybeRedirectInsideOpenFolder() {
120151
const nextItem = this.env.linearItems[this.treeId][this.linearIndex + 1];
121-
const redirectToFirstChild =
152+
const redirectInsideOpenFolder =
122153
!this.env.canDropBelowOpenFolders &&
123154
nextItem &&
124155
this.targetItem.depth === nextItem.depth - 1 &&
125156
this.offset === 'bottom';
126-
if (redirectToFirstChild) {
157+
if (redirectInsideOpenFolder) {
127158
this.targetItem = nextItem;
128159
this.linearIndex += 1;
129160
this.offset = 'top';
130161
}
162+
}
163+
164+
/**
165+
* Inside a folder, only drop at bottom offset to make it visually
166+
* consistent.
167+
*/
168+
private maybeMapToBottomOffset() {
169+
const priorItem = this.env.linearItems[this.treeId][this.linearIndex - 1];
170+
if (
171+
this.offset === 'top' &&
172+
this.targetItem.depth === (priorItem?.depth ?? -1)
173+
) {
174+
this.offset = 'bottom';
175+
this.linearIndex -= 1;
176+
}
177+
}
131178

132-
const { depth } = this.targetItem;
179+
private canDropAtCurrentTarget() {
133180
const targetItemData = this.env.items[this.targetItem.item];
134181

135182
if (
136183
!this.offset &&
137184
!this.env.canDropOnNonFolder &&
138185
!targetItemData.isFolder
139186
) {
140-
return undefined;
187+
return false;
141188
}
142189

143190
if (!this.offset && !this.env.canDropOnFolder && targetItemData.isFolder) {
144-
return undefined;
191+
return false;
145192
}
146193

147194
if (this.offset && !this.env.canReorderItems) {
148-
return undefined;
195+
return false;
149196
}
150197

151-
const { parent } = this.evaluator.getParentOfLinearItem(
152-
this.linearIndex,
153-
this.treeId
154-
);
155-
156198
if (
157-
this.evaluator.draggingItems.some(
199+
this.evaluator.draggingItems?.some(
158200
draggingItem => draggingItem.index === this.targetItem.item
159201
)
202+
) {
203+
return false;
204+
}
205+
206+
return true;
207+
}
208+
209+
getDraggingPosition(): DraggingPosition | undefined {
210+
if (this.env.linearItems[this.treeId].length === 0) {
211+
return this.getEmptyTreeDragPosition();
212+
}
213+
214+
if (
215+
!this.evaluator.draggingItems ||
216+
this.linearIndex < 0 ||
217+
this.linearIndex >= this.env.linearItems[this.treeId].length
160218
) {
161219
return undefined;
162220
}
163221

222+
this.maybeRedirectToParent();
223+
224+
if (this.areDraggingItemsDescendantOfTarget()) {
225+
return undefined;
226+
}
227+
228+
const reparented = this.maybeReparentUpwards();
229+
if (reparented) {
230+
return reparented;
231+
}
232+
233+
this.maybeRedirectInsideOpenFolder();
234+
235+
// Must run before maybeMapToBottomOffset
236+
const { parent } = this.evaluator.getParentOfLinearItem(
237+
this.linearIndex,
238+
this.treeId
239+
);
164240
const newChildIndex =
165241
this.env.items[parent.item].children!.indexOf(this.targetItem.item) +
166242
(this.offset === 'top' ? 0 : 1);
167243

168-
if (
169-
this.offset === 'top' &&
170-
depth ===
171-
(this.env.linearItems[this.treeId][this.linearIndex - 1]?.depth ?? -1)
172-
) {
173-
this.offset = 'bottom';
174-
this.linearIndex -= 1;
244+
this.maybeMapToBottomOffset();
245+
246+
if (!this.canDropAtCurrentTarget()) {
247+
return undefined;
175248
}
176249

250+
// used to be here: this.maybeMapToBottomOffset();.. moved up for better readability
251+
177252
if (this.offset) {
178253
return {
179254
targetType: 'between-items',
@@ -215,4 +290,15 @@ export class DraggingPositionEvaluation {
215290

216291
return this.isDescendant(treeId, parentLinearIndex, potentialParents);
217292
}
293+
294+
private areDraggingItemsDescendantOfTarget() {
295+
return (
296+
this.evaluator.draggingItems &&
297+
this.isDescendant(
298+
this.treeId,
299+
this.linearIndex,
300+
this.evaluator.draggingItems
301+
)
302+
);
303+
}
218304
}

packages/core/src/controlledEnvironment/DraggingPositionEvaluator.ts

+8
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,14 @@ export class DraggingPositionEvaluator {
101101
treeLinearItems.length - 1
102102
);
103103

104+
if (treeLinearItems.length === 0) {
105+
return {
106+
linearIndex: 0,
107+
offset: 'bottom',
108+
indentation: 0,
109+
};
110+
}
111+
104112
const targetLinearItem = treeLinearItems[linearIndex];
105113
const targetItem = this.env.items[targetLinearItem.item];
106114

packages/core/src/stories/BasicExamples.stories.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,24 @@ export const UnitTestTreeOpen = () => (
496496
</UncontrolledTreeEnvironment>
497497
);
498498

499+
export const ReparentTestTree = () => (
500+
<UncontrolledTreeEnvironment<string>
501+
canDragAndDrop
502+
canDropOnFolder
503+
canReorderItems
504+
dataProvider={
505+
new StaticTreeDataProvider(buildTestTree(), (item, data) => ({
506+
...item,
507+
data,
508+
}))
509+
}
510+
getItemTitle={item => `${item.data}`}
511+
viewState={{ 'tree-1': { expandedItems: ['a', 'ad'] } }}
512+
>
513+
<Tree treeId="tree-1" rootItem="root" treeLabel="Tree Example" />
514+
</UncontrolledTreeEnvironment>
515+
);
516+
499517
export const DisableMultiselect = () => (
500518
<UncontrolledTreeEnvironment<string>
501519
canDragAndDrop

0 commit comments

Comments
 (0)