Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

N8N-3304 drag and drop from nodes panel #3123

Merged
merged 9 commits into from
Apr 19, 2022
Merged
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