Skip to content

Commit 11a9bb5

Browse files
Merge pull request frappe#304 from ruchamahabal/drop-placeholder
2 parents 658ab1d + 455822d commit 11a9bb5

7 files changed

Lines changed: 378 additions & 102 deletions

File tree

frontend/src/components/BlockEditor.vue

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const showResizer = computed(() => {
5252
return (
5353
!props.block.isRoot() &&
5454
!props.editable &&
55+
!store.isDragging &&
5556
isBlockSelected.value &&
5657
!blockController.multipleBlocksSelected() &&
5758
!props.block.getParentBlock()?.isGrid() &&
@@ -93,6 +94,7 @@ const showPaddingHandler = computed(() => {
9394
return (
9495
isBlockSelected.value &&
9596
!resizing.value &&
97+
!store.isDragging &&
9698
!props.editable &&
9799
!blockController.multipleBlocksSelected() &&
98100
!props.block.isSVG() &&
@@ -104,6 +106,7 @@ const showMarginHandler = computed(() => {
104106
return (
105107
isBlockSelected.value &&
106108
!props.block.isRoot() &&
109+
!store.isDragging &&
107110
!resizing.value &&
108111
!props.editable &&
109112
!blockController.multipleBlocksSelected() &&
@@ -120,6 +123,7 @@ const showBorderRadiusHandler = computed(() => {
120123
!props.block.isSVG() &&
121124
!props.editable &&
122125
!resizing.value &&
126+
!store.isDragging &&
123127
!blockController.multipleBlocksSelected()
124128
);
125129
});
@@ -147,6 +151,7 @@ watchEffect(() => {
147151
store.showRightPanel;
148152
store.showLeftPanel;
149153
store.activeBreakpoint;
154+
store.dropTarget.placeholder;
150155
canvasProps.breakpoints.map((bp) => bp.visible);
151156
nextTick(() => {
152157
updateTracker.value();
@@ -167,7 +172,13 @@ const getStyleClasses = computed(() => {
167172
} else {
168173
classes.push("ring-blue-400");
169174
}
170-
if (isBlockSelected.value && !props.editable && !props.block.isRoot() && !props.block.isRepeater()) {
175+
if (
176+
isBlockSelected.value &&
177+
!props.editable &&
178+
!props.block.isRoot() &&
179+
!props.block.isRepeater() &&
180+
!store.isDragging
181+
) {
171182
// make editor interactive
172183
classes.push("pointer-events-auto");
173184
// Place the block on the top of the stack

frontend/src/components/BuilderAssets.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,14 @@ useEventListener(componentContainer, "dragstart", (e) => {
8989
const component = (e.target as HTMLElement)?.closest(".user-component") as HTMLElement;
9090
if (component) {
9191
setComponentData(e, component.dataset.componentName as string);
92+
store.handleDragStart(e);
9293
}
9394
});
9495
96+
useEventListener(componentContainer, "dragend", () => {
97+
store.handleDragEnd();
98+
});
99+
95100
useEventListener(componentContainer, "dblclick", (e) => {
96101
const component = (e.target as HTMLElement)?.closest(".user-component") as HTMLElement;
97102
if (component) {

frontend/src/components/BuilderBlockTemplates.vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
draggable="true"
2727
@click="selectBlockTemplate(blockTemplate)"
2828
@dblclick="is_developer_mode && store.editBlockTemplate(blockTemplate.name)"
29-
@dragstart="(ev) => setBlockTemplateData(ev, blockTemplate)">
29+
@dragstart="(ev) => setBlockTemplateData(ev, blockTemplate)"
30+
@dragend="() => store.handleDragEnd()">
3031
<div
3132
class="flex h-4/5 items-center justify-center"
3233
:class="{
@@ -69,7 +70,8 @@ const blockTemplates = computed(() => {
6970
});
7071
7172
const setBlockTemplateData = (ev: DragEvent, component: BlockTemplate) => {
72-
ev?.dataTransfer?.setData("blockTemplate", component.name);
73+
ev.dataTransfer?.setData("blockTemplate", component.name);
74+
store.handleDragStart(ev);
7375
};
7476
7577
const selectedBlockTemplate = ref<string | null>(null);

frontend/src/components/BuilderCanvas.vue

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
<template>
22
<div ref="canvasContainer" @click="handleClick">
33
<slot name="header"></slot>
4-
<div class="overlay absolute" id="overlay" ref="overlay" />
4+
<div
5+
class="overlay absolute"
6+
:class="{ 'pointer-events-none': isOverDropZone }"
7+
id="overlay"
8+
ref="overlay" />
59
<Transition name="fade">
610
<div
711
class="absolute bottom-0 left-0 right-0 top-0 z-[19] grid w-full place-items-center bg-surface-gray-1 p-10 text-ink-gray-5"
@@ -10,9 +14,6 @@
1014
</div>
1115
</Transition>
1216
<BlockSnapGuides></BlockSnapGuides>
13-
<div
14-
v-if="isOverDropZone"
15-
class="pointer-events-none absolute bottom-0 left-0 right-0 top-0 z-30 bg-cyan-300 opacity-20"></div>
1617
<div
1718
class="fixed flex gap-40"
1819
ref="canvas"
@@ -299,4 +300,14 @@ const renderedBreakpoints = computed(() => canvasProps.breakpoints.filter((bp) =
299300
.fade-leave-to {
300301
opacity: 0;
301302
}
303+
304+
#placeholder {
305+
@apply transition-all;
306+
}
307+
.vertical-placeholder {
308+
@apply mx-4 h-full min-h-5 w-auto border-l-2 border-dashed border-blue-500;
309+
}
310+
.horizontal-placeholder {
311+
@apply my-4 h-auto w-full border-t-2 border-dashed border-blue-500;
312+
}
302313
</style>

frontend/src/store.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,15 @@ const useStore = defineStore("store", {
9494
"Media",
9595
"Advanced",
9696
] as BlockTemplate["category"][],
97+
isDragging: false,
98+
isDropping: false,
99+
dropTarget: {
100+
x: <number | null>null,
101+
y: <number | null>null,
102+
placeholder: <HTMLElement | null>null,
103+
parentBlock: <Block | null>null,
104+
index: <number | null>null,
105+
}
97106
}),
98107
actions: {
99108
clearBlocks() {
@@ -546,6 +555,75 @@ const useStore = defineStore("store", {
546555
fragmentId: null,
547556
};
548557
},
558+
// drag and drop
559+
handleDragStart(ev: DragEvent) {
560+
if (ev.target && ev.dataTransfer) {
561+
this.isDragging = true;
562+
const ghostScale = this.activeCanvas?.canvasProps.scale;
563+
564+
// Clone the entire draggable element
565+
const dragElement = (ev.target as HTMLElement)
566+
if (!dragElement) return;
567+
const ghostDiv = document.createElement("div");
568+
const ghostElement = dragElement.cloneNode(true) as HTMLElement;
569+
ghostDiv.appendChild(ghostElement);
570+
ghostDiv.id = "ghost";
571+
ghostDiv.style.position = "fixed";
572+
ghostDiv.style.transform = `scale(${ghostScale || 1})`;
573+
ghostDiv.style.pointerEvents = "none";
574+
ghostDiv.style.zIndex = "99999";
575+
// Append the ghostDiv to the DOM
576+
document.body.appendChild(ghostDiv);
577+
578+
// Wait for the next frame to ensure the ghostDiv is rendered
579+
requestAnimationFrame(() => {
580+
ev.dataTransfer?.setDragImage(ghostDiv, 0, 0);
581+
// Clean up the ghostDiv after a short delay
582+
setTimeout(() => {
583+
document.body.removeChild(ghostDiv);
584+
}, 0);
585+
});
586+
this.insertDropPlaceholder();
587+
}
588+
},
589+
handleDragEnd() {
590+
// check flag to avoid race condition with async onDrop
591+
if (!this.isDropping) {
592+
this.resetDropTarget();
593+
}
594+
},
595+
resetDropTarget() {
596+
this.removeDropPlaceholder();
597+
this.dropTarget = {
598+
x: null,
599+
y: null,
600+
placeholder: null,
601+
parentBlock: null,
602+
index: null,
603+
}
604+
this.isDragging = false
605+
this.isDropping = false
606+
},
607+
insertDropPlaceholder() {
608+
// append placeholder component to the dom directly
609+
// to avoid re-rendering the whole canvas
610+
if (this.dropTarget.placeholder) return;
611+
612+
let element = document.createElement("div");
613+
element.id = "placeholder";
614+
615+
const root = document.querySelector(".__builder_component__[data-block-id='root']");
616+
if (root) {
617+
this.dropTarget.placeholder = root.appendChild(element);
618+
}
619+
return this.dropTarget.placeholder;
620+
},
621+
removeDropPlaceholder() {
622+
const placeholder = document.getElementById("placeholder")
623+
if (placeholder) {
624+
placeholder.remove()
625+
}
626+
}
549627
},
550628
});
551629

frontend/src/utils/helpers.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,31 @@ function generateId() {
444444
return Math.random().toString(36).substr(2, 9);
445445
}
446446

447+
function throttle<T extends (...args: any[]) => void>(func: T, wait: number = 1000) {
448+
let timeout: ReturnType<typeof setTimeout> | null = null
449+
let lastArgs: Parameters<T> | null = null
450+
let pending = false
451+
452+
const invoke = (...args: Parameters<T>) => {
453+
lastArgs = args
454+
if (timeout) {
455+
pending = true
456+
return
457+
}
458+
459+
func(...lastArgs);
460+
timeout = setTimeout(() => {
461+
timeout = null
462+
if (pending && lastArgs) {
463+
pending = false
464+
invoke(...lastArgs)
465+
}
466+
}, wait)
467+
};
468+
469+
return invoke
470+
}
471+
447472
export {
448473
addPxToNumber,
449474
alert,
@@ -478,4 +503,5 @@ export {
478503
RGBToHex,
479504
stripExtension,
480505
uploadImage,
506+
throttle,
481507
};

0 commit comments

Comments
 (0)