fix(vue): distinguish layer child drops

This commit is contained in:
Danila Poyarkov 2026-05-13 05:10:29 +03:00
parent 1f273f880e
commit 49620ef26b
4 changed files with 91 additions and 18 deletions

View file

@ -21,9 +21,15 @@ const emit = defineEmits<{
}>()
const editor = useEditor()
function expandNode(id: string) {
if (!expanded.value.includes(id)) expanded.value = [...expanded.value, id]
}
const { draggingId, instruction, instructionTargetId, setupItem } = useLayerDrag(
editor,
indentPerLevel
indentPerLevel,
expandNode
)
function buildTree(parentId: string): LayerNode[] {
@ -108,7 +114,7 @@ function toggleExpand(id: string) {
emit('toggleExpand', id)
const idx = expanded.value.indexOf(id)
if (idx !== -1) expanded.value = expanded.value.filter((e) => e !== id)
else expanded.value = [...expanded.value, id]
else expandNode(id)
}
function getKey(node: LayerNode) {

View file

@ -24,7 +24,11 @@ interface DragItem {
type TreeInstruction = LayerDragInstruction
export function useLayerDrag(editor: Editor, indentPerLevel = 16) {
export function useLayerDrag(
editor: Editor,
indentPerLevel = 16,
onMakeChildDrop?: (targetId: string) => void
) {
const draggingId = ref<string | null>(null)
const instruction = ref<TreeInstruction | null>(null)
const instructionTargetId = ref<string | null>(null)
@ -36,6 +40,7 @@ export function useLayerDrag(editor: Editor, indentPerLevel = 16) {
const data = item()
const isContainer = editor.graph.isContainer(data.id)
const mode: ItemMode = data.hasChildren ? 'expanded' : 'standard'
const cleanup = combine(
@ -60,14 +65,19 @@ export function useLayerDrag(editor: Editor, indentPerLevel = 16) {
indentPerLevel,
currentLevel: data.level,
mode,
block: ['make-child', 'reparent']
block: isContainer ? ['reparent'] : ['make-child', 'reparent']
}
),
canDrop: ({ source }) => source.data.id !== data.id,
onDrag: ({ self }) => {
const inst = extractInstruction(self.data) as TreeInstruction | null
instruction.value = inst
instructionTargetId.value = inst ? data.id : null
const inst = extractInstruction(self.data)
if (!inst || inst.type === 'instruction-blocked') {
instruction.value = null
instructionTargetId.value = null
return
}
instruction.value = inst as TreeInstruction
instructionTargetId.value = data.id
},
onDragLeave: () => {
instruction.value = null
@ -92,8 +102,10 @@ export function useLayerDrag(editor: Editor, indentPerLevel = 16) {
const sourceId = source.data.id as string
const targetId = target.data.id as string
const inst = extractInstruction(target.data) as TreeInstruction | null
if (!inst || !sourceId || !targetId) return
const rawInstruction = extractInstruction(target.data)
if (!rawInstruction || rawInstruction.type === 'instruction-blocked') return
const inst = rawInstruction as TreeInstruction
if (!sourceId || !targetId) return
if (editor.graph.isDescendant(targetId, sourceId)) return
@ -110,7 +122,9 @@ export function useLayerDrag(editor: Editor, indentPerLevel = 16) {
editor.reorderChildWithUndo(sourceId, targetParentId, targetIndex + 1)
} else {
const container = editor.graph.getNode(targetId)
editor.reorderChildWithUndo(sourceId, targetId, container?.childIds.length ?? 0)
if (!container || !editor.graph.isContainer(targetId)) return
editor.reorderChildWithUndo(sourceId, targetId, container.childIds.length)
onMakeChildDrop?.(targetId)
}
draggingId.value = null

View file

@ -111,7 +111,7 @@ function onTreeSelect(e: CustomEvent, select: (additive: boolean) => void) {
: 'bg-transparent text-surface hover:bg-hover',
draggingId === node.id ? 'opacity-30' : '',
instructionTargetId === node.id && instruction?.type === 'make-child'
? 'ring-2 ring-accent ring-inset'
? 'bg-accent/15 text-surface outline-2 outline-accent outline-offset-[-2px]'
: '',
!node.visible ? 'opacity-50' : ''
]"
@ -181,7 +181,16 @@ function onTreeSelect(e: CustomEvent, select: (additive: boolean) => void) {
</Tip>
</span>
<!-- DnD indicator -->
<div
v-if="instructionTargetId === node.id && instruction?.type === 'make-child'"
class="pointer-events-none absolute inset-y-1 rounded border border-accent bg-accent/10"
:style="{
left: `${item.level * INDENT}px`,
right: '4px'
}"
/>
<!-- DnD reorder indicator -->
<div
v-if="
instructionTargetId === node.id &&

View file

@ -1,7 +1,15 @@
import { expect, test } from '@playwright/test'
import { expect, test, type Page } from '@playwright/test'
import { CanvasHelper } from '#tests/helpers/canvas'
async function layerOrder(page: Page, parentId?: string) {
return page.evaluate((id) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.graph.getNode(id ?? store.state.currentPageId)?.childIds ?? []
}, parentId)
}
test('dragging layers reorders scene nodes', async ({ page }) => {
await page.goto('/')
const canvas = new CanvasHelper(page)
@ -26,12 +34,48 @@ test('dragging layers reorders scene nodes', async ({ page }) => {
await source.dragTo(target, { targetPosition: { x: 20, y: 2 } })
await canvas.waitForRender()
const order = await page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.graph.getNode(store.state.currentPageId)?.childIds ?? []
})
const order = await layerOrder(page)
expect(order).toEqual([ids.third, ids.first, ids.second])
canvas.assertNoErrors()
})
test('dragging a layer into a container expands it and shows the child', async ({ page }) => {
await page.goto('/')
const canvas = new CanvasHelper(page)
await canvas.waitForInit()
canvas.errors.length = 0
await canvas.clearCanvas()
const ids = await page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const pageId = store.state.currentPageId
const frame = store.graph.createNode('FRAME', pageId, {
name: 'Drop Frame',
x: 0,
y: 0,
width: 200,
height: 120
})
const rect = store.graph.createNode('RECTANGLE', pageId, {
name: 'Child Candidate',
x: 240,
y: 0,
width: 80,
height: 80
})
store.requestRender()
return { frame: frame.id, rect: rect.id }
})
await canvas.waitForRender()
const source = page.locator(`[data-node-id="${ids.rect}"] [data-test-id="layers-item"]`)
const target = page.locator(`[data-node-id="${ids.frame}"] [data-test-id="layers-item"]`)
await source.dragTo(target, { targetPosition: { x: 70, y: 12 } })
await canvas.waitForRender()
await expect(page.locator(`[data-node-id="${ids.rect}"] [data-test-id="layers-item"]`)).toBeVisible()
expect(await layerOrder(page, ids.frame)).toEqual([ids.rect])
canvas.assertNoErrors()
})