Skip to content

Commit

Permalink
feat(editor): Add drag and drop from nodes panel (#3123)
Browse files Browse the repository at this point in the history
* ✨ Added support for drag and drop from nodes main panel.

✨ Added node draggable placeholder.

* ✨ Added snapping to grid. Changed how draggable ghost follows the cursor.

* 💄 Changed node drag anchor position to be centered.

* ✨ Added drag and drop animation. Added event cancellation when dropping node on main panel.

* ♻️ Simplified drag and drop code and cleaned up prop-drilling.

* 🐛 Added check for nodeTypeName in dataTransfer when draging and dropping nodes.

* 🐛 Ensured MS Edge compatibility. MS edge does not send datatransfer in ondragover event.

Co-authored-by: Mutasem <mutdmour@gmail.com>
  • Loading branch information
alexgrozav and mutdmour committed Apr 19, 2022
1 parent f4e9562 commit f566569
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 61 deletions.
10 changes: 6 additions & 4 deletions packages/editor-ui/src/components/NodeCreator/CreatorItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
clickable: props.clickable,
active: props.active,
}"
@click="listeners['click']"
>
@click="listeners.click"
>
<CategoryItem
v-if="props.item.type === 'category'"
:item="props.item"
Expand All @@ -21,7 +21,9 @@
v-else-if="props.item.type === 'node'"
:nodeType="props.item.properties.nodeType"
:bordered="!props.lastNode"
></NodeItem>
@dragstart="listeners.dragstart"
@dragend="listeners.dragend"
/>
</div>
</template>

Expand Down Expand Up @@ -54,4 +56,4 @@ export default {
}
}
</style>
</style>
15 changes: 11 additions & 4 deletions packages/editor-ui/src/components/NodeCreator/ItemIterator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,22 @@
@before-leave="beforeLeave"
@leave="leave"
>
<div v-for="(item, index) in elements" :key="item.key" :class="item.type" :data-key="item.key">
<div
v-for="(item, index) in elements"
:key="item.key"
:class="item.type"
:data-key="item.key"
>
<CreatorItem
:item="item"
:active="activeIndex === index && !disabled"
:clickable="!disabled"
:lastNode="
index === elements.length - 1 || elements[index + 1].type !== 'node'
"
@click="() => selected(item)"
@click="$emit('selected', item)"
@dragstart="emit('dragstart', item, $event)"
@dragend="emit('dragend', item, $event)"
/>
</div>
</div>
Expand All @@ -36,12 +43,12 @@ export default Vue.extend({
},
props: ['elements', 'activeIndex', 'disabled', 'transitionsEnabled'],
methods: {
selected(element: INodeCreateElement) {
emit(eventName: string, element: INodeCreateElement, event: Event) {
if (this.$props.disabled) {
return;
}
this.$emit('selected', element);
this.$emit(eventName, { element, event });
},
beforeEnter(el: HTMLElement) {
el.style.height = '0';
Expand Down
28 changes: 18 additions & 10 deletions packages/editor-ui/src/components/NodeCreator/MainPanel.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
<template>
<div @click="onClickInside" class="container">
<div
class="container"
ref="mainPanelContainer"
@click="onClickInside"
>
<SlideTransition>
<SubcategoryPanel v-if="activeSubcategory" :elements="subcategorizedNodes" :title="activeSubcategory.properties.subcategory" :activeIndex="activeSubcategoryIndex" @close="onSubcategoryClose" @selected="selected" />
<SubcategoryPanel
v-if="activeSubcategory"
:elements="subcategorizedNodes"
:title="activeSubcategory.properties.subcategory"
:activeIndex="activeSubcategoryIndex"
@close="onSubcategoryClose"
@selected="selected"
/>
</SlideTransition>
<div class="main-panel">
<SearchBar
Expand Down Expand Up @@ -35,7 +46,10 @@
@selected="selected"
/>
</div>
<NoResults v-else @nodeTypeSelected="nodeTypeSelected" />
<NoResults
v-else
@nodeTypeSelected="$emit('nodeTypeSelected', $event)"
/>
</div>
</div>
</template>
Expand All @@ -56,7 +70,6 @@ import { ALL_NODE_FILTER, CORE_NODES_CATEGORY, REGULAR_NODE_FILTER, TRIGGER_NODE
import SlideTransition from '../transitions/SlideTransition.vue';
import { matchesNodeType, matchesSelectType } from './helpers';
export default mixins(externalHooks).extend({
name: 'NodeCreateList',
components: {
Expand Down Expand Up @@ -235,18 +248,13 @@ export default mixins(externalHooks).extend({
},
selected(element: INodeCreateElement) {
if (element.type === 'node') {
const properties = element.properties as INodeItemProps;
this.nodeTypeSelected(properties.nodeType.name);
this.$emit('nodeTypeSelected', (element.properties as INodeItemProps).nodeType.name);
} else if (element.type === 'category') {
this.onCategorySelected(element.category);
} else if (element.type === 'subcategory') {
this.onSubcategorySelected(element);
}
},
nodeTypeSelected(nodeTypeName: string) {
this.$emit('nodeTypeSelected', nodeTypeName);
},
onCategorySelected(category: string) {
if (this.activeCategory.includes(category)) {
this.activeCategory = this.activeCategory.filter(
Expand Down
32 changes: 30 additions & 2 deletions packages/editor-ui/src/components/NodeCreator/NodeCreator.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
<template>
<div>
<SlideTransition>
<div class="node-creator" v-if="active" v-click-outside="onClickOutside">
<MainPanel @nodeTypeSelected="nodeTypeSelected" :categorizedItems="categorizedItems" :categoriesWithNodes="categoriesWithNodes" :searchItems="searchItems"></MainPanel>
<div
v-if="active"
class="node-creator"
ref="nodeCreator"
v-click-outside="onClickOutside"
@dragover="onDragOver"
@drop="onDrop"
>
<MainPanel
@nodeTypeSelected="nodeTypeSelected"
:categorizedItems="categorizedItems"
:categoriesWithNodes="categoriesWithNodes"
:searchItems="searchItems"
/>
</div>
</SlideTransition>
</div>
Expand Down Expand Up @@ -94,6 +106,22 @@ export default Vue.extend({
nodeTypeSelected (nodeTypeName: string) {
this.$emit('nodeTypeSelected', nodeTypeName);
},
onDragOver(event: DragEvent) {
event.preventDefault();
},
onDrop(event: DragEvent) {
if (!event.dataTransfer) {
return;
}
const nodeTypeName = event.dataTransfer.getData('nodeTypeName');
const nodeCreatorBoundingRect = (this.$refs.nodeCreator as Element).getBoundingClientRect();
// Abort drag end event propagation if dropped inside nodes panel
if (nodeTypeName && event.pageX >= nodeCreatorBoundingRect.x && event.pageY >= nodeCreatorBoundingRect.y) {
event.stopPropagation();
}
},
},
watch: {
nodeTypes(newList) {
Expand Down
125 changes: 118 additions & 7 deletions packages/editor-ui/src/components/NodeCreator/NodeItem.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<template>
<div :class="{[$style['node-item']]: true, [$style.bordered]: bordered}">
<div
draggable
@dragstart="onDragStart"
@dragend="onDragEnd"
:class="{[$style['node-item']]: true, [$style.bordered]: bordered}"
>
<NodeIcon :class="$style['node-icon']" :nodeType="nodeType" />
<div>
<div :class="$style.details">
Expand All @@ -11,7 +16,7 @@
}}
</span>
<span :class="$style['trigger-icon']">
<TriggerIcon v-if="$options.isTrigger(nodeType)" />
<TriggerIcon v-if="isTrigger" />
</span>
</div>
<div :class="$style.description">
Expand All @@ -21,14 +26,26 @@
})
}}
</div>

<div :class="$style['draggable-data-transfer']" ref="draggableDataTransfer" />
<transition name="node-item-transition">
<div
:class="$style.draggable"
:style="draggableStyle"
ref="draggable"
v-show="dragging"
>
<NodeIcon class="node-icon" :nodeType="nodeType" :size="40" :shrink="false" />
</div>
</transition>
</div>
</div>
</template>

<script lang="ts">
import {getNewNodePosition, NODE_SIZE} from '@/views/canvasHelpers';
import Vue from 'vue';
import { INodeTypeDescription } from 'n8n-workflow';
import NodeIcon from '../NodeIcon.vue';
import TriggerIcon from '../TriggerIcon.vue';
Expand All @@ -44,14 +61,73 @@ export default Vue.extend({
'nodeType',
'bordered',
],
data() {
return {
dragging: false,
draggablePosition: {
x: -100,
y: -100,
},
};
},
computed: {
shortNodeType() {
shortNodeType(): string {
return this.$locale.shortNodeType(this.nodeType.name);
},
isTrigger (): boolean {
return this.nodeType.group.includes('trigger');
},
draggableStyle(): { top: string; left: string; } {
return {
top: `${this.draggablePosition.y}px`,
left: `${this.draggablePosition.x}px`,
};
},
},
// @ts-ignore
isTrigger (nodeType: INodeTypeDescription): boolean {
return nodeType.group.includes('trigger');
mounted() {
/**
* Workaround for firefox, that doesn't attach the pageX and pageY coordinates to "ondrag" event.
* All browsers attach the correct page coordinates to the "dragover" event.
* @bug https://bugzilla.mozilla.org/show_bug.cgi?id=505521
*/
document.body.addEventListener("dragover", this.onDragOver);
},
destroyed() {
document.body.removeEventListener("dragover", this.onDragOver);
},
methods: {
onDragStart(event: DragEvent): void {
const { pageX: x, pageY: y } = event;
this.$emit('dragstart', event);
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = "copy";
event.dataTransfer.dropEffect = "copy";
event.dataTransfer.setData('nodeTypeName', this.nodeType.name);
event.dataTransfer.setDragImage(this.$refs.draggableDataTransfer as Element, 0, 0);
}
this.dragging = true;
this.draggablePosition = { x, y };
},
onDragOver(event: DragEvent): void {
if (!this.dragging || event.pageX === 0 && event.pageY === 0) {
return;
}
const [x,y] = getNewNodePosition([], [event.pageX - NODE_SIZE / 2, event.pageY - NODE_SIZE / 2]);
this.draggablePosition = { x, y };
},
onDragEnd(event: DragEvent): void {
this.$emit('dragend', event);
this.dragging = false;
setTimeout(() => {
this.draggablePosition = { x: -100, y: -100 };
}, 300);
},
},
});
</script>
Expand Down Expand Up @@ -100,4 +176,39 @@ export default Vue.extend({
display: flex;
}
.draggable {
width: 100px;
height: 100px;
position: fixed;
z-index: 1;
opacity: 0.66;
border: 2px solid var(--color-foreground-xdark);
border-radius: var(--border-radius-large);
background-color: var(--color-background-xlight);
display: flex;
justify-content: center;
align-items: center;
}
.draggable-data-transfer {
width: 1px;
height: 1px;
}
</style>

<style lang="scss" scoped>
.node-item-transition {
&-enter-active,
&-leave-active {
transition-property: opacity, transform;
transition-duration: 300ms;
transition-timing-function: ease;
}
&-enter,
&-leave-to {
opacity: 0;
transform: scale(0);
}
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
<ItemIterator
:elements="elements"
:activeIndex="activeIndex"
@selected="selected"
@selected="$emit('selected', $event)"
@dragstart="$emit('dragstart', $event)"
@dragend="$emit('dragend', $event)"
/>
</div>
</div>
Expand All @@ -38,9 +40,6 @@ export default Vue.extend({
},
},
methods: {
selected(element: INodeCreateElement) {
this.$emit('selected', element);
},
onBackArrowClick() {
this.$emit('close');
},
Expand Down Expand Up @@ -101,4 +100,4 @@ export default Vue.extend({
}
}
</style>
</style>
Loading

0 comments on commit f566569

Please sign in to comment.