Skip to content

Commit

Permalink
Reparent out of grid (#6264)
Browse files Browse the repository at this point in the history
**Problem:**

It's currently not possible to reparent out of a grid.

**Fix:**

Allow reparenting out of a grid while holding `cmd`, like all other
reparenting scenarios.


https://github.com/user-attachments/assets/df75b0ee-5098-4cba-b66c-507697cf03c0

**Notes:**
The grid rearrange strategy now needs to act as a sort of meta strat,
because 1) it needs a custom mouse drag handling (due to the grid
placeholder controls) and 2) cells don't have positioning props so
absolute reparenting needs some massaging in order to work

Fixes #6263
  • Loading branch information
ruggi authored and liady committed Dec 13, 2024
1 parent 88349e6 commit 3c31ac6
Show file tree
Hide file tree
Showing 6 changed files with 550 additions and 298 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
CanvasStrategy,
CustomStrategyState,
InteractionCanvasState,
InteractionLifecycle,
} from '../canvas-strategy-types'
import {
controlWithProps,
Expand Down Expand Up @@ -61,7 +62,6 @@ export function baseAbsoluteReparentStrategy(
return null
}

const dragInteractionData = interactionSession.interactionData // Why TypeScript?!
const filteredSelectedElements = flattenSelection(selectedElements)
const isApplicable = replaceFragmentLikePathsWithTheirChildrenRecursive(
canvasState.startingMetadata,
Expand Down Expand Up @@ -90,127 +90,156 @@ export function baseAbsoluteReparentStrategy(
category: 'modalities',
type: 'reparent-large',
},
controlsToRender: [
controlWithProps({
control: ParentOutlines,
props: { targetParent: reparentTarget.newParent.intendedParentPath },
key: 'parent-outlines-control',
show: 'visible-only-while-active',
}),
controlWithProps({
control: ParentBounds,
props: { targetParent: reparentTarget.newParent.intendedParentPath },
key: 'parent-bounds-control',
show: 'visible-only-while-active',
}),
controlWithProps({
control: ZeroSizedElementControls,
props: { showAllPossibleElements: true },
key: 'zero-size-control',
show: 'visible-only-while-active',
}),
],
controlsToRender: controlsForAbsoluteReparent(reparentTarget),
fitness: shouldKeepMovingDraggedGroupChildren(
canvasState.startingMetadata,
selectedElements,
reparentTarget.newParent,
)
? 1
: fitness,
apply: (strategyLifecycle) => {
const { projectContents, nodeModules } = canvasState
const newParent = reparentTarget.newParent
return ifAllowedToReparent(
canvasState,
canvasState.startingMetadata,
filteredSelectedElements,
newParent.intendedParentPath,
() => {
if (dragInteractionData.drag == null) {
return emptyStrategyApplicationResult
}
apply: applyAbsoluteReparent(
canvasState,
interactionSession,
customStrategyState,
reparentTarget,
filteredSelectedElements,
),
}
}
}

const allowedToReparent = filteredSelectedElements.every((selectedElement) => {
return isAllowedToReparent(
canvasState.projectContents,
canvasState.startingMetadata,
selectedElement,
newParent.intendedParentPath,
)
})
export function controlsForAbsoluteReparent(reparentTarget: ReparentTarget) {
return [
controlWithProps({
control: ParentOutlines,
props: { targetParent: reparentTarget.newParent.intendedParentPath },
key: 'parent-outlines-control',
show: 'visible-only-while-active',
}),
controlWithProps({
control: ParentBounds,
props: { targetParent: reparentTarget.newParent.intendedParentPath },
key: 'parent-bounds-control',
show: 'visible-only-while-active',
}),
controlWithProps({
control: ZeroSizedElementControls,
props: { showAllPossibleElements: true },
key: 'zero-size-control',
show: 'visible-only-while-active',
}),
]
}

if (reparentTarget.shouldReparent && allowedToReparent) {
const commands = mapDropNulls(
(selectedElement) =>
createAbsoluteReparentAndOffsetCommands(
selectedElement,
newParent,
null,
canvasState.startingMetadata,
canvasState.startingElementPathTree,
canvasState.startingAllElementProps,
canvasState.builtInDependencies,
projectContents,
nodeModules,
'force-pins',
),
selectedElements,
)
export function applyAbsoluteReparent(
canvasState: InteractionCanvasState,
interactionSession: InteractionSession,
customStrategyState: CustomStrategyState,
reparentTarget: ReparentTarget,
selectedElements: ElementPath[],
) {
return (strategyLifecycle: InteractionLifecycle) => {
if (
selectedElements.length === 0 ||
interactionSession == null ||
interactionSession.interactionData.type !== 'DRAG'
) {
return emptyStrategyApplicationResult
}
const dragInteractionData = interactionSession.interactionData

const { projectContents, nodeModules } = canvasState
const newParent = reparentTarget.newParent
return ifAllowedToReparent(
canvasState,
canvasState.startingMetadata,
selectedElements,
newParent.intendedParentPath,
() => {
if (dragInteractionData.drag == null) {
return emptyStrategyApplicationResult
}

const allowedToReparent = selectedElements.every((selectedElement) => {
return isAllowedToReparent(
canvasState.projectContents,
canvasState.startingMetadata,
selectedElement,
newParent.intendedParentPath,
)
})

let newPaths: Array<ElementPath> = []
let updatedTargetPaths: UpdatedPathMap = {}
if (reparentTarget.shouldReparent && allowedToReparent) {
const commands = mapDropNulls(
(selectedElement) =>
createAbsoluteReparentAndOffsetCommands(
selectedElement,
newParent,
null,
canvasState.startingMetadata,
canvasState.startingElementPathTree,
canvasState.startingAllElementProps,
canvasState.builtInDependencies,
projectContents,
nodeModules,
'force-pins',
),
selectedElements,
)

let newPaths: Array<ElementPath> = []
let updatedTargetPaths: UpdatedPathMap = {}

commands.forEach((c) => {
newPaths.push(c.newPath)
updatedTargetPaths[EP.toString(c.oldPath)] = c.newPath
})
commands.forEach((c) => {
newPaths.push(c.newPath)
updatedTargetPaths[EP.toString(c.oldPath)] = c.newPath
})

const moveCommands =
absoluteMoveStrategy(
canvasState,
{
...interactionSession,
updatedTargetPaths: updatedTargetPaths,
},
{ ...defaultCustomStrategyState(), action: 'reparent' },
)?.strategy.apply(strategyLifecycle).commands ?? []
const moveCommands =
absoluteMoveStrategy(
canvasState,
{
...interactionSession,
updatedTargetPaths: updatedTargetPaths,
},
{ ...defaultCustomStrategyState(), action: 'reparent' },
)?.strategy.apply(strategyLifecycle).commands ?? []

const elementsToRerender = EP.uniqueElementPaths([
...customStrategyState.elementsToRerender,
...newPaths,
...newPaths.map(EP.parentPath),
...filteredSelectedElements.map(EP.parentPath),
])
return strategyApplicationResult(
[
...moveCommands,
...commands.flatMap((c) => c.commands),
updateSelectedViews('always', newPaths),
setElementsToRerenderCommand(elementsToRerender),
...maybeAddContainLayout(
canvasState.startingMetadata,
canvasState.startingAllElementProps,
canvasState.startingElementPathTree,
newParent.intendedParentPath,
),
setCursorCommand(CSSCursor.Reparent),
],
{
elementsToRerender,
},
)
} else {
const moveCommands =
absoluteMoveStrategy(canvasState, interactionSession, {
...defaultCustomStrategyState(),
action: 'reparent',
})?.strategy.apply(strategyLifecycle).commands ?? []
return strategyApplicationResult(moveCommands)
}
},
)
const elementsToRerender = EP.uniqueElementPaths([
...customStrategyState.elementsToRerender,
...newPaths,
...newPaths.map(EP.parentPath),
...selectedElements.map(EP.parentPath),
])
return strategyApplicationResult(
[
...moveCommands,
...commands.flatMap((c) => c.commands),
updateSelectedViews('always', newPaths),
setElementsToRerenderCommand(elementsToRerender),
...maybeAddContainLayout(
canvasState.startingMetadata,
canvasState.startingAllElementProps,
canvasState.startingElementPathTree,
newParent.intendedParentPath,
),
setCursorCommand(CSSCursor.Reparent),
],
{
elementsToRerender,
},
)
} else {
const moveCommands =
absoluteMoveStrategy(canvasState, interactionSession, {
...defaultCustomStrategyState(),
action: 'reparent',
})?.strategy.apply(strategyLifecycle).commands ?? []
return strategyApplicationResult(moveCommands)
}
},
}
)
}
}

Expand Down
Loading

0 comments on commit 3c31ac6

Please sign in to comment.