diff --git a/frontend/src/components/ComponentLayers.vue b/frontend/src/components/ComponentLayers.vue
index 3f4c164e..e1401278 100644
--- a/frontend/src/components/ComponentLayers.vue
+++ b/frontend/src/components/ComponentLayers.vue
@@ -110,8 +110,14 @@
v-for="(slot, slotName) in element.componentSlots"
:key="slot.slotId"
:data-slot-layer-id="slot.slotId"
+ :data-slot-name="slotName"
+ :data-slot-parent-id="slot.parentBlockId"
:title="slot.slotName"
class="relative min-w-24 cursor-pointer select-none rounded border border-transparent bg-surface-base bg-opacity-50 text-base text-ink-gray-6"
+ :class="{
+ 'border-outline-blue-5 !bg-surface-blue-2 dark:!bg-surface-blue-10':
+ canvasStore.layerDraggingOverSlot === slot.slotId,
+ }"
@click.stop="canvasStore.activeCanvas?.selectSlot(slot)"
>
{
+ Object.assign(dragState, { draggedElement: null, hoverTarget: null, hoverPosition: null, hoverSlot: null })
+}
const resetDropIndicators = () => {
showDropIndicator.value = false
canvasStore.layerDraggingOverBlock = null
+ canvasStore.layerDraggingOverSlot = null
}
const checkMove = () => false // Prevent automatic reordering
@@ -327,10 +339,18 @@ const onMouseMove = (event: MouseEvent) => {
if (!draggedElement) return
const target = document.elementFromPoint(event.clientX, event.clientY)
+ const slotRow = target?.closest("[data-slot-layer-id]") as HTMLElement | null
const layerItem = target?.closest(".component-layer-item") as HTMLElement | null
+ // Hovering a slot header row (deeper than the nearest block row) targets the slot itself
+ if (slotRow && (!layerItem || layerItem.contains(slotRow)) && !draggedElement.contains(slotRow)) {
+ hoverSlot(slotRow)
+ return
+ }
+
if (!layerItem || layerItem === draggedElement || draggedElement.contains(layerItem)) {
resetDropIndicators()
+ dragState.hoverSlot = null
return
}
@@ -348,6 +368,7 @@ const onMouseMove = (event: MouseEvent) => {
const isInCenterZone = relativeY > elementHeight * 0.25 && relativeY < elementHeight * 0.75
dragState.hoverTarget = layerItem
+ dragState.hoverSlot = null
if (block.canHaveChildren() && isInCenterZone) {
// Highlight parent block for nested drop
@@ -361,6 +382,17 @@ const onMouseMove = (event: MouseEvent) => {
}
}
+const hoverSlot = (slotRow: HTMLElement) => {
+ resetDropIndicators()
+ canvasStore.layerDraggingOverSlot = slotRow.dataset.slotLayerId!
+ dragState.hoverTarget = null
+ dragState.hoverPosition = null
+ dragState.hoverSlot = {
+ parentId: slotRow.dataset.slotParentId!,
+ slotName: slotRow.dataset.slotName!,
+ }
+}
+
const removeFromParent = (block: Block) => {
const parent = block.getParentBlock()
if (parent) {
@@ -368,6 +400,16 @@ const removeFromParent = (block: Block) => {
}
}
+const moveBlockIntoSlot = (draggedBlock: Block, parentBlock: Block, slotName: string) => {
+ const slot = parentBlock.getSlot(slotName)
+ if (!slot) return
+ removeFromParent(draggedBlock)
+ draggedBlock.parentBlock = parentBlock
+ draggedBlock.parentSlotName = slotName
+ if (!Array.isArray(slot.slotContent)) slot.slotContent = []
+ slot.slotContent.push(draggedBlock)
+}
+
const moveBlockInside = (draggedBlock: Block, targetBlock: Block) => {
removeFromParent(draggedBlock)
if (!targetBlock.children) targetBlock.children = []
@@ -402,27 +444,34 @@ const onDragEnd = (e: DragEvent) => {
resetDropIndicators()
document.removeEventListener("mousemove", onMouseMove)
- const { draggedElement, hoverTarget, hoverPosition } = dragState
- if (!draggedElement || !hoverTarget || !hoverPosition || draggedElement.contains(hoverTarget)) {
- Object.assign(dragState, { draggedElement: null, hoverTarget: null, hoverPosition: null })
+ const { draggedElement, hoverTarget, hoverPosition, hoverSlot } = dragState
+ clearDragState()
+
+ const draggedBlock =
+ draggedElement && canvasStore.activeCanvas?.findBlock(draggedElement.dataset.componentLayerId!)
+ if (!draggedBlock) return
+
+ if (hoverSlot) {
+ const parentBlock = canvasStore.activeCanvas?.findBlock(hoverSlot.parentId)
+ if (parentBlock && parentBlock !== draggedBlock) {
+ moveBlockIntoSlot(draggedBlock, parentBlock, hoverSlot.slotName)
+ canvasStore.activeCanvas?.selectBlock(draggedBlock, e)
+ }
return
}
- const draggedBlock = canvasStore.activeCanvas?.findBlock(draggedElement.dataset.componentLayerId!)
- const targetBlock = canvasStore.activeCanvas?.findBlock(hoverTarget.dataset.componentLayerId!)
+ if (!hoverTarget || !hoverPosition || draggedElement!.contains(hoverTarget)) return
- if (draggedBlock && targetBlock && draggedBlock !== targetBlock) {
+ const targetBlock = canvasStore.activeCanvas?.findBlock(hoverTarget.dataset.componentLayerId!)
+ if (targetBlock && draggedBlock !== targetBlock) {
if (hoverPosition === "inside") {
moveBlockInside(draggedBlock, targetBlock)
} else {
moveBlockAdjacent(draggedBlock, targetBlock, hoverPosition)
}
-
// Select the moved block
canvasStore.activeCanvas?.selectBlock(draggedBlock, e)
}
-
- Object.assign(dragState, { draggedElement: null, hoverTarget: null, hoverPosition: null })
}
// @ts-ignore
diff --git a/frontend/src/stores/canvasStore.ts b/frontend/src/stores/canvasStore.ts
index 5ee838c9..f21dd9b5 100644
--- a/frontend/src/stores/canvasStore.ts
+++ b/frontend/src/stores/canvasStore.ts
@@ -50,6 +50,7 @@ const useCanvasStore = defineStore("canvasStore", () => {
// drag & drop
const isDragging = ref(false)
const layerDraggingOverBlock = ref(null)
+ const layerDraggingOverSlot = ref(null)
const dropTarget = reactive({
x: null as number | null,
y: null as number | null,
@@ -191,6 +192,7 @@ const useCanvasStore = defineStore("canvasStore", () => {
dropTarget,
isDragging,
layerDraggingOverBlock,
+ layerDraggingOverSlot,
handleDragStart,
handleDragEnd,
// fragment mode
diff --git a/frontend/src/utils/useStudioEvents.ts b/frontend/src/utils/useStudioEvents.ts
index df234fb4..8c85e0db 100644
--- a/frontend/src/utils/useStudioEvents.ts
+++ b/frontend/src/utils/useStudioEvents.ts
@@ -98,13 +98,24 @@ export function useStudioEvents() {
if (isTargetEditable(e)) return
// delete
- if ((e.key === "Backspace" || e.key === "Delete") && blockController.isAnyBlockSelected()) {
- for (const block of blockController.getSelectedBlocks()) {
- canvasStore.activeCanvas?.removeBlock(block, e.shiftKey)
+ if (e.key === "Backspace" || e.key === "Delete") {
+ // a selected slot takes precedence over its (also-selected) parent block
+ const selectedSlot = canvasStore.activeCanvas?.selectedSlot
+ if (selectedSlot) {
+ const parent = canvasStore.activeCanvas?.findBlock(selectedSlot.parentBlockId)
+ parent?.removeSlot(selectedSlot.slotName)
+ e.stopPropagation()
+ return
+ }
+
+ if (blockController.isAnyBlockSelected()) {
+ for (const block of blockController.getSelectedBlocks()) {
+ canvasStore.activeCanvas?.removeBlock(block, e.shiftKey)
+ }
+ clearSelection()
+ e.stopPropagation()
+ return
}
- clearSelection()
- e.stopPropagation()
- return
}
// duplicate