fix(vue): distinguish layer child drops
This commit is contained in:
parent
1f273f880e
commit
49620ef26b
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue