mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
feat(editor): Add drag and drop from nodes panel (#3123)
* ✨ 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>
This commit is contained in:
parent
f4e9562451
commit
f566569299
|
@ -5,8 +5,8 @@
|
||||||
clickable: props.clickable,
|
clickable: props.clickable,
|
||||||
active: props.active,
|
active: props.active,
|
||||||
}"
|
}"
|
||||||
@click="listeners['click']"
|
@click="listeners.click"
|
||||||
>
|
>
|
||||||
<CategoryItem
|
<CategoryItem
|
||||||
v-if="props.item.type === 'category'"
|
v-if="props.item.type === 'category'"
|
||||||
:item="props.item"
|
:item="props.item"
|
||||||
|
@ -21,7 +21,9 @@
|
||||||
v-else-if="props.item.type === 'node'"
|
v-else-if="props.item.type === 'node'"
|
||||||
:nodeType="props.item.properties.nodeType"
|
:nodeType="props.item.properties.nodeType"
|
||||||
:bordered="!props.lastNode"
|
:bordered="!props.lastNode"
|
||||||
></NodeItem>
|
@dragstart="listeners.dragstart"
|
||||||
|
@dragend="listeners.dragend"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -54,4 +56,4 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -8,7 +8,12 @@
|
||||||
@before-leave="beforeLeave"
|
@before-leave="beforeLeave"
|
||||||
@leave="leave"
|
@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
|
<CreatorItem
|
||||||
:item="item"
|
:item="item"
|
||||||
:active="activeIndex === index && !disabled"
|
:active="activeIndex === index && !disabled"
|
||||||
|
@ -16,7 +21,9 @@
|
||||||
:lastNode="
|
:lastNode="
|
||||||
index === elements.length - 1 || elements[index + 1].type !== 'node'
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,12 +43,12 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
props: ['elements', 'activeIndex', 'disabled', 'transitionsEnabled'],
|
props: ['elements', 'activeIndex', 'disabled', 'transitionsEnabled'],
|
||||||
methods: {
|
methods: {
|
||||||
selected(element: INodeCreateElement) {
|
emit(eventName: string, element: INodeCreateElement, event: Event) {
|
||||||
if (this.$props.disabled) {
|
if (this.$props.disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$emit('selected', element);
|
this.$emit(eventName, { element, event });
|
||||||
},
|
},
|
||||||
beforeEnter(el: HTMLElement) {
|
beforeEnter(el: HTMLElement) {
|
||||||
el.style.height = '0';
|
el.style.height = '0';
|
||||||
|
|
|
@ -1,7 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<div @click="onClickInside" class="container">
|
<div
|
||||||
|
class="container"
|
||||||
|
ref="mainPanelContainer"
|
||||||
|
@click="onClickInside"
|
||||||
|
>
|
||||||
<SlideTransition>
|
<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>
|
</SlideTransition>
|
||||||
<div class="main-panel">
|
<div class="main-panel">
|
||||||
<SearchBar
|
<SearchBar
|
||||||
|
@ -35,7 +46,10 @@
|
||||||
@selected="selected"
|
@selected="selected"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<NoResults v-else @nodeTypeSelected="nodeTypeSelected" />
|
<NoResults
|
||||||
|
v-else
|
||||||
|
@nodeTypeSelected="$emit('nodeTypeSelected', $event)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -56,7 +70,6 @@ import { ALL_NODE_FILTER, CORE_NODES_CATEGORY, REGULAR_NODE_FILTER, TRIGGER_NODE
|
||||||
import SlideTransition from '../transitions/SlideTransition.vue';
|
import SlideTransition from '../transitions/SlideTransition.vue';
|
||||||
import { matchesNodeType, matchesSelectType } from './helpers';
|
import { matchesNodeType, matchesSelectType } from './helpers';
|
||||||
|
|
||||||
|
|
||||||
export default mixins(externalHooks).extend({
|
export default mixins(externalHooks).extend({
|
||||||
name: 'NodeCreateList',
|
name: 'NodeCreateList',
|
||||||
components: {
|
components: {
|
||||||
|
@ -235,18 +248,13 @@ export default mixins(externalHooks).extend({
|
||||||
},
|
},
|
||||||
selected(element: INodeCreateElement) {
|
selected(element: INodeCreateElement) {
|
||||||
if (element.type === 'node') {
|
if (element.type === 'node') {
|
||||||
const properties = element.properties as INodeItemProps;
|
this.$emit('nodeTypeSelected', (element.properties as INodeItemProps).nodeType.name);
|
||||||
|
|
||||||
this.nodeTypeSelected(properties.nodeType.name);
|
|
||||||
} else if (element.type === 'category') {
|
} else if (element.type === 'category') {
|
||||||
this.onCategorySelected(element.category);
|
this.onCategorySelected(element.category);
|
||||||
} else if (element.type === 'subcategory') {
|
} else if (element.type === 'subcategory') {
|
||||||
this.onSubcategorySelected(element);
|
this.onSubcategorySelected(element);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
nodeTypeSelected(nodeTypeName: string) {
|
|
||||||
this.$emit('nodeTypeSelected', nodeTypeName);
|
|
||||||
},
|
|
||||||
onCategorySelected(category: string) {
|
onCategorySelected(category: string) {
|
||||||
if (this.activeCategory.includes(category)) {
|
if (this.activeCategory.includes(category)) {
|
||||||
this.activeCategory = this.activeCategory.filter(
|
this.activeCategory = this.activeCategory.filter(
|
||||||
|
|
|
@ -1,8 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<SlideTransition>
|
<SlideTransition>
|
||||||
<div class="node-creator" v-if="active" v-click-outside="onClickOutside">
|
<div
|
||||||
<MainPanel @nodeTypeSelected="nodeTypeSelected" :categorizedItems="categorizedItems" :categoriesWithNodes="categoriesWithNodes" :searchItems="searchItems"></MainPanel>
|
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>
|
</div>
|
||||||
</SlideTransition>
|
</SlideTransition>
|
||||||
</div>
|
</div>
|
||||||
|
@ -94,6 +106,22 @@ export default Vue.extend({
|
||||||
nodeTypeSelected (nodeTypeName: string) {
|
nodeTypeSelected (nodeTypeName: string) {
|
||||||
this.$emit('nodeTypeSelected', nodeTypeName);
|
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: {
|
watch: {
|
||||||
nodeTypes(newList) {
|
nodeTypes(newList) {
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
<template>
|
<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" />
|
<NodeIcon :class="$style['node-icon']" :nodeType="nodeType" />
|
||||||
<div>
|
<div>
|
||||||
<div :class="$style.details">
|
<div :class="$style.details">
|
||||||
|
@ -11,7 +16,7 @@
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span :class="$style['trigger-icon']">
|
<span :class="$style['trigger-icon']">
|
||||||
<TriggerIcon v-if="$options.isTrigger(nodeType)" />
|
<TriggerIcon v-if="isTrigger" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.description">
|
<div :class="$style.description">
|
||||||
|
@ -21,14 +26,26 @@
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
|
import {getNewNodePosition, NODE_SIZE} from '@/views/canvasHelpers';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { INodeTypeDescription } from 'n8n-workflow';
|
|
||||||
|
|
||||||
import NodeIcon from '../NodeIcon.vue';
|
import NodeIcon from '../NodeIcon.vue';
|
||||||
import TriggerIcon from '../TriggerIcon.vue';
|
import TriggerIcon from '../TriggerIcon.vue';
|
||||||
|
@ -44,14 +61,73 @@ export default Vue.extend({
|
||||||
'nodeType',
|
'nodeType',
|
||||||
'bordered',
|
'bordered',
|
||||||
],
|
],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dragging: false,
|
||||||
|
draggablePosition: {
|
||||||
|
x: -100,
|
||||||
|
y: -100,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
shortNodeType() {
|
shortNodeType(): string {
|
||||||
return this.$locale.shortNodeType(this.nodeType.name);
|
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
|
mounted() {
|
||||||
isTrigger (nodeType: INodeTypeDescription): boolean {
|
/**
|
||||||
return nodeType.group.includes('trigger');
|
* 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>
|
</script>
|
||||||
|
@ -100,4 +176,39 @@ export default Vue.extend({
|
||||||
display: flex;
|
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>
|
</style>
|
||||||
|
|
|
@ -13,7 +13,9 @@
|
||||||
<ItemIterator
|
<ItemIterator
|
||||||
:elements="elements"
|
:elements="elements"
|
||||||
:activeIndex="activeIndex"
|
:activeIndex="activeIndex"
|
||||||
@selected="selected"
|
@selected="$emit('selected', $event)"
|
||||||
|
@dragstart="$emit('dragstart', $event)"
|
||||||
|
@dragend="$emit('dragend', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,9 +40,6 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
selected(element: INodeCreateElement) {
|
|
||||||
this.$emit('selected', element);
|
|
||||||
},
|
|
||||||
onBackArrowClick() {
|
onBackArrowClick() {
|
||||||
this.$emit('close');
|
this.$emit('close');
|
||||||
},
|
},
|
||||||
|
@ -101,4 +100,4 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="node-view-root">
|
<div
|
||||||
|
class="node-view-root"
|
||||||
|
@dragover="onDragOver"
|
||||||
|
@drop="onDrop"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="node-view-wrapper"
|
class="node-view-wrapper"
|
||||||
:class="workflowClasses"
|
:class="workflowClasses"
|
||||||
|
@ -10,27 +14,31 @@
|
||||||
v-touch:tap="touchTap"
|
v-touch:tap="touchTap"
|
||||||
@mouseup="mouseUp"
|
@mouseup="mouseUp"
|
||||||
@wheel="wheelScroll"
|
@wheel="wheelScroll"
|
||||||
|
>
|
||||||
|
<div id="node-view-background" class="node-view-background" :style="backgroundStyle" />
|
||||||
|
<div
|
||||||
|
id="node-view"
|
||||||
|
class="node-view"
|
||||||
|
:style="workflowStyle"
|
||||||
>
|
>
|
||||||
<div id="node-view-background" class="node-view-background" :style="backgroundStyle"></div>
|
|
||||||
<div id="node-view" class="node-view" :style="workflowStyle">
|
|
||||||
<node
|
<node
|
||||||
v-for="nodeData in nodes"
|
v-for="nodeData in nodes"
|
||||||
@duplicateNode="duplicateNode"
|
@duplicateNode="duplicateNode"
|
||||||
@deselectAllNodes="deselectAllNodes"
|
@deselectAllNodes="deselectAllNodes"
|
||||||
@deselectNode="nodeDeselectedByName"
|
@deselectNode="nodeDeselectedByName"
|
||||||
@nodeSelected="nodeSelectedByName"
|
@nodeSelected="nodeSelectedByName"
|
||||||
@removeNode="removeNode"
|
@removeNode="removeNode"
|
||||||
@runWorkflow="runWorkflow"
|
@runWorkflow="runWorkflow"
|
||||||
@moved="onNodeMoved"
|
@moved="onNodeMoved"
|
||||||
@run="onNodeRun"
|
@run="onNodeRun"
|
||||||
:id="'node-' + getNodeIndex(nodeData.name)"
|
:id="'node-' + getNodeIndex(nodeData.name)"
|
||||||
:key="getNodeIndex(nodeData.name)"
|
:key="getNodeIndex(nodeData.name)"
|
||||||
:name="nodeData.name"
|
:name="nodeData.name"
|
||||||
:isReadOnly="isReadOnly"
|
:isReadOnly="isReadOnly"
|
||||||
:instance="instance"
|
:instance="instance"
|
||||||
:isActive="!!activeNode && activeNode.name === nodeData.name"
|
:isActive="!!activeNode && activeNode.name === nodeData.name"
|
||||||
:hideActions="pullConnActive"
|
:hideActions="pullConnActive"
|
||||||
></node>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DataDisplay :renaming="renamingActive" @valueChanged="valueChanged"/>
|
<DataDisplay :renaming="renamingActive" @valueChanged="valueChanged"/>
|
||||||
|
@ -41,7 +49,7 @@
|
||||||
:active="createNodeActive"
|
:active="createNodeActive"
|
||||||
@nodeTypeSelected="nodeTypeSelected"
|
@nodeTypeSelected="nodeTypeSelected"
|
||||||
@closeNodeCreator="closeNodeCreator"
|
@closeNodeCreator="closeNodeCreator"
|
||||||
></node-creator>
|
/>
|
||||||
<div :class="{ 'zoom-menu': true, 'regular-zoom-menu': !isDemo, 'demo-zoom-menu': isDemo, expanded: !sidebarMenuCollapsed }">
|
<div :class="{ 'zoom-menu': true, 'regular-zoom-menu': !isDemo, 'demo-zoom-menu': isDemo, expanded: !sidebarMenuCollapsed }">
|
||||||
<button @click="zoomToFit" class="button-white" :title="$locale.baseText('nodeView.zoomToFit')">
|
<button @click="zoomToFit" class="button-white" :title="$locale.baseText('nodeView.zoomToFit')">
|
||||||
<font-awesome-icon icon="expand"/>
|
<font-awesome-icon icon="expand"/>
|
||||||
|
@ -177,6 +185,11 @@ import {
|
||||||
import '../plugins/N8nCustomConnectorType';
|
import '../plugins/N8nCustomConnectorType';
|
||||||
import '../plugins/PlusEndpointType';
|
import '../plugins/PlusEndpointType';
|
||||||
|
|
||||||
|
interface AddNodeOptions {
|
||||||
|
position?: XYPosition;
|
||||||
|
dragAndDrop?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export default mixins(
|
export default mixins(
|
||||||
copyPaste,
|
copyPaste,
|
||||||
externalHooks,
|
externalHooks,
|
||||||
|
@ -1227,6 +1240,27 @@ export default mixins(
|
||||||
this.createNodeActive = false;
|
this.createNodeActive = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onDragOver(event: DragEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
},
|
||||||
|
|
||||||
|
onDrop(event: DragEvent) {
|
||||||
|
if (!event.dataTransfer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeTypeName = event.dataTransfer.getData('nodeTypeName');
|
||||||
|
if (nodeTypeName) {
|
||||||
|
const mousePosition = this.getMousePositionWithinNodeView(event);
|
||||||
|
|
||||||
|
this.addNodeButton(nodeTypeName, {
|
||||||
|
position: [mousePosition[0] - CanvasHelpers.NODE_SIZE / 2, mousePosition[1] - CanvasHelpers.NODE_SIZE / 2],
|
||||||
|
dragAndDrop: true,
|
||||||
|
});
|
||||||
|
this.createNodeActive = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
nodeDeselectedByName (nodeName: string) {
|
nodeDeselectedByName (nodeName: string) {
|
||||||
const node = this.$store.getters.getNodeByName(nodeName);
|
const node = this.$store.getters.getNodeByName(nodeName);
|
||||||
if (node) {
|
if (node) {
|
||||||
|
@ -1267,7 +1301,7 @@ export default mixins(
|
||||||
duration: 0,
|
duration: 0,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async injectNode (nodeTypeName: string) {
|
async injectNode (nodeTypeName: string, options: AddNodeOptions = {}) {
|
||||||
const nodeTypeData: INodeTypeDescription | null = this.$store.getters.nodeType(nodeTypeName);
|
const nodeTypeData: INodeTypeDescription | null = this.$store.getters.nodeType(nodeTypeName);
|
||||||
|
|
||||||
if (nodeTypeData === null) {
|
if (nodeTypeData === null) {
|
||||||
|
@ -1297,7 +1331,10 @@ export default mixins(
|
||||||
|
|
||||||
// when pulling new connection from node or injecting into a connection
|
// when pulling new connection from node or injecting into a connection
|
||||||
const lastSelectedNode = this.lastSelectedNode;
|
const lastSelectedNode = this.lastSelectedNode;
|
||||||
if (lastSelectedNode) {
|
|
||||||
|
if (options.position) {
|
||||||
|
newNodeData.position = CanvasHelpers.getNewNodePosition(this.nodes, options.position);
|
||||||
|
} else if (lastSelectedNode) {
|
||||||
const lastSelectedConnection = this.lastSelectedConnection;
|
const lastSelectedConnection = this.lastSelectedConnection;
|
||||||
if (lastSelectedConnection) { // set when injecting into a connection
|
if (lastSelectedConnection) { // set when injecting into a connection
|
||||||
const [diffX] = CanvasHelpers.getConnectorLengths(lastSelectedConnection);
|
const [diffX] = CanvasHelpers.getConnectorLengths(lastSelectedConnection);
|
||||||
|
@ -1308,10 +1345,12 @@ export default mixins(
|
||||||
|
|
||||||
// set when pulling connections
|
// set when pulling connections
|
||||||
if (this.newNodeInsertPosition) {
|
if (this.newNodeInsertPosition) {
|
||||||
newNodeData.position = CanvasHelpers.getNewNodePosition(this.nodes, [this.newNodeInsertPosition[0] + CanvasHelpers.GRID_SIZE, this.newNodeInsertPosition[1] - CanvasHelpers.NODE_SIZE / 2]);
|
newNodeData.position = CanvasHelpers.getNewNodePosition(this.nodes, [
|
||||||
|
this.newNodeInsertPosition[0] + CanvasHelpers.GRID_SIZE,
|
||||||
|
this.newNodeInsertPosition[1] - CanvasHelpers.NODE_SIZE / 2,
|
||||||
|
]);
|
||||||
this.newNodeInsertPosition = null;
|
this.newNodeInsertPosition = null;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
let yOffset = 0;
|
let yOffset = 0;
|
||||||
|
|
||||||
if (lastSelectedConnection) {
|
if (lastSelectedConnection) {
|
||||||
|
@ -1353,7 +1392,11 @@ export default mixins(
|
||||||
this.$store.commit('setStateDirty', true);
|
this.$store.commit('setStateDirty', true);
|
||||||
|
|
||||||
this.$externalHooks().run('nodeView.addNodeButton', { nodeTypeName });
|
this.$externalHooks().run('nodeView.addNodeButton', { nodeTypeName });
|
||||||
this.$telemetry.trackNodesPanel('nodeView.addNodeButton', { node_type: nodeTypeName, workflow_id: this.$store.getters.workflowId });
|
this.$telemetry.trackNodesPanel('nodeView.addNodeButton', {
|
||||||
|
node_type: nodeTypeName,
|
||||||
|
workflow_id: this.$store.getters.workflowId,
|
||||||
|
drag_and_drop: options.dragAndDrop,
|
||||||
|
} as IDataObject);
|
||||||
|
|
||||||
// Automatically deselect all nodes and select the current one and also active
|
// Automatically deselect all nodes and select the current one and also active
|
||||||
// current node
|
// current node
|
||||||
|
@ -1396,7 +1439,7 @@ export default mixins(
|
||||||
|
|
||||||
this.__addConnection(connectionData, true);
|
this.__addConnection(connectionData, true);
|
||||||
},
|
},
|
||||||
async addNodeButton (nodeTypeName: string) {
|
async addNodeButton (nodeTypeName: string, options: AddNodeOptions = {}) {
|
||||||
if (this.editAllowedCheck() === false) {
|
if (this.editAllowedCheck() === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1405,7 +1448,7 @@ export default mixins(
|
||||||
const lastSelectedNode = this.lastSelectedNode;
|
const lastSelectedNode = this.lastSelectedNode;
|
||||||
const lastSelectedNodeOutputIndex = this.$store.getters.lastSelectedNodeOutputIndex;
|
const lastSelectedNodeOutputIndex = this.$store.getters.lastSelectedNodeOutputIndex;
|
||||||
|
|
||||||
const newNodeData = await this.injectNode(nodeTypeName);
|
const newNodeData = await this.injectNode(nodeTypeName, options);
|
||||||
if (!newNodeData) {
|
if (!newNodeData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue