mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-23 10:32:17 -08:00
refactor(editor): Convert Draggable components to composition API (no-changelog) (#9889)
Co-authored-by: Ricardo Espinoza <ricardo@n8n.io>
This commit is contained in:
parent
8debac755e
commit
ae67d6b753
|
@ -317,19 +317,12 @@ describe('Data mapping', () => {
|
|||
workflowPage.actions.zoomToFit();
|
||||
|
||||
workflowPage.actions.openNode('Set');
|
||||
ndv.actions.clearParameterInput('value');
|
||||
cy.get('body').type('{esc}');
|
||||
|
||||
ndv.getters.parameterInput('includeOtherFields').find('input[type="checkbox"]').should('exist');
|
||||
ndv.getters.parameterInput('includeOtherFields').find('input[type="text"]').should('not.exist');
|
||||
ndv.getters
|
||||
.inputDataContainer()
|
||||
.should('exist')
|
||||
.find('span')
|
||||
.contains('count')
|
||||
.realMouseDown()
|
||||
.realMouseMove(100, 100);
|
||||
cy.wait(50);
|
||||
const pill = ndv.getters.inputDataContainer().find('span').contains('count');
|
||||
pill.should('be.visible');
|
||||
pill.realMouseDown();
|
||||
pill.realMouseMove(100, 100);
|
||||
|
||||
ndv.getters
|
||||
.parameterInput('includeOtherFields')
|
||||
|
@ -340,13 +333,13 @@ describe('Data mapping', () => {
|
|||
.find('input[type="text"]')
|
||||
.should('exist')
|
||||
.invoke('css', 'border')
|
||||
.then((border) => expect(border).to.include('dashed rgb(90, 76, 194)'));
|
||||
.should('include', 'dashed rgb(90, 76, 194)');
|
||||
|
||||
ndv.getters
|
||||
.parameterInput('value')
|
||||
.find('input[type="text"]')
|
||||
.should('exist')
|
||||
.invoke('css', 'border')
|
||||
.then((border) => expect(border).to.include('dashed rgb(90, 76, 194)'));
|
||||
.should('include', 'dashed rgb(90, 76, 194)');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -101,7 +101,7 @@ describe('AssignmentCollection.vue', () => {
|
|||
setActivePinia(pinia);
|
||||
|
||||
const { getByTestId, findAllByTestId } = renderComponent({ pinia });
|
||||
const dropArea = getByTestId('assignment-collection-drop-area');
|
||||
const dropArea = getByTestId('drop-area');
|
||||
|
||||
await dropAssignment({ key: 'boolKey', value: true, dropArea });
|
||||
await dropAssignment({ key: 'stringKey', value: 'stringValue', dropArea });
|
||||
|
|
|
@ -9,154 +9,131 @@
|
|||
|
||||
<Teleport to="body">
|
||||
<div v-show="isDragging" ref="draggable" :class="$style.draggable" :style="draggableStyle">
|
||||
<slot name="preview" :can-drop="canDrop" :el="draggingEl"></slot>
|
||||
<slot name="preview" :can-drop="canDrop" :el="draggingElement"></slot>
|
||||
</div>
|
||||
</Teleport>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { XYPosition } from '@/Interface';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { mapStores } from 'pinia';
|
||||
import { defineComponent } from 'vue';
|
||||
import { isPresent } from '@/utils/typesUtils';
|
||||
import { type StyleValue, computed, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Draggable',
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
data: {
|
||||
type: String,
|
||||
},
|
||||
tag: {
|
||||
type: String,
|
||||
default: 'div',
|
||||
},
|
||||
targetDataKey: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const draggablePosition = {
|
||||
x: -100,
|
||||
y: -100,
|
||||
};
|
||||
type Props = {
|
||||
type: string;
|
||||
data?: string;
|
||||
tag?: string;
|
||||
targetDataKey?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
return {
|
||||
isDragging: false,
|
||||
draggablePosition,
|
||||
draggingEl: null as null | HTMLElement,
|
||||
draggableStyle: {
|
||||
transform: `translate(${draggablePosition.x}px, ${draggablePosition.y}px)`,
|
||||
},
|
||||
animationFrameId: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useNDVStore),
|
||||
canDrop(): boolean {
|
||||
return this.ndvStore.canDraggableDrop;
|
||||
},
|
||||
stickyPosition(): XYPosition | null {
|
||||
return this.ndvStore.draggableStickyPos;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setDraggableStyle() {
|
||||
this.draggableStyle = {
|
||||
transform: `translate(${this.draggablePosition.x}px, ${this.draggablePosition.y}px)`,
|
||||
};
|
||||
},
|
||||
onDragStart(e: MouseEvent) {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), { tag: 'div', disabled: false });
|
||||
const emit = defineEmits<{
|
||||
(event: 'drag', value: XYPosition): void;
|
||||
(event: 'dragstart', value: HTMLElement): void;
|
||||
(event: 'dragend', value: HTMLElement): void;
|
||||
}>();
|
||||
|
||||
this.draggingEl = e.target as HTMLElement;
|
||||
if (this.targetDataKey && this.draggingEl.dataset?.target !== this.targetDataKey) {
|
||||
this.draggingEl = this.draggingEl.closest(
|
||||
`[data-target="${this.targetDataKey}"]`,
|
||||
) as HTMLElement;
|
||||
}
|
||||
const isDragging = ref(false);
|
||||
const draggingElement = ref<HTMLElement>();
|
||||
const draggablePosition = ref<XYPosition>([0, 0]);
|
||||
const animationFrameId = ref<number>();
|
||||
const ndvStore = useNDVStore();
|
||||
|
||||
if (this.targetDataKey && this.draggingEl?.dataset?.target !== this.targetDataKey) {
|
||||
return;
|
||||
}
|
||||
const draggableStyle = computed<StyleValue>(() => ({
|
||||
transform: `translate(${draggablePosition.value[0]}px, ${draggablePosition.value[1]}px)`,
|
||||
}));
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const canDrop = computed(() => ndvStore.canDraggableDrop);
|
||||
|
||||
this.isDragging = false;
|
||||
this.draggablePosition = { x: e.pageX, y: e.pageY };
|
||||
this.setDraggableStyle();
|
||||
const stickyPosition = computed(() => ndvStore.draggableStickyPos);
|
||||
|
||||
window.addEventListener('mousemove', this.onDrag);
|
||||
window.addEventListener('mouseup', this.onDragEnd);
|
||||
const onDragStart = (event: MouseEvent) => {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// blur so that any focused inputs update value
|
||||
const activeElement = document.activeElement as HTMLElement;
|
||||
if (activeElement) {
|
||||
activeElement.blur();
|
||||
}
|
||||
},
|
||||
onDrag(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
draggingElement.value = event.target as HTMLElement;
|
||||
if (props.targetDataKey && draggingElement.value.dataset?.target !== props.targetDataKey) {
|
||||
draggingElement.value = draggingElement.value.closest(
|
||||
`[data-target="${props.targetDataKey}"]`,
|
||||
) as HTMLElement;
|
||||
}
|
||||
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
if (props.targetDataKey && draggingElement.value?.dataset?.target !== props.targetDataKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isDragging) {
|
||||
this.isDragging = true;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const data =
|
||||
this.targetDataKey && this.draggingEl ? this.draggingEl.dataset.value : this.data || '';
|
||||
this.ndvStore.draggableStartDragging({
|
||||
type: this.type,
|
||||
data: data || '',
|
||||
dimensions: this.draggingEl?.getBoundingClientRect() ?? null,
|
||||
});
|
||||
isDragging.value = false;
|
||||
draggablePosition.value = [event.pageX, event.pageY];
|
||||
|
||||
this.$emit('dragstart', this.draggingEl);
|
||||
document.body.style.cursor = 'grabbing';
|
||||
}
|
||||
window.addEventListener('mousemove', onDrag);
|
||||
window.addEventListener('mouseup', onDragEnd);
|
||||
|
||||
this.animationFrameId = window.requestAnimationFrame(() => {
|
||||
if (this.canDrop && this.stickyPosition) {
|
||||
this.draggablePosition = { x: this.stickyPosition[0], y: this.stickyPosition[1] };
|
||||
} else {
|
||||
this.draggablePosition = { x: e.pageX, y: e.pageY };
|
||||
}
|
||||
this.setDraggableStyle();
|
||||
this.$emit('drag', this.draggablePosition);
|
||||
});
|
||||
},
|
||||
onDragEnd() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
// blur so that any focused inputs update value
|
||||
const activeElement = document.activeElement as HTMLElement;
|
||||
if (activeElement) {
|
||||
activeElement.blur();
|
||||
}
|
||||
};
|
||||
|
||||
document.body.style.cursor = 'unset';
|
||||
window.removeEventListener('mousemove', this.onDrag);
|
||||
window.removeEventListener('mouseup', this.onDragEnd);
|
||||
window.cancelAnimationFrame(this.animationFrameId);
|
||||
const onDrag = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
setTimeout(() => {
|
||||
this.$emit('dragend', this.draggingEl);
|
||||
this.isDragging = false;
|
||||
this.draggingEl = null;
|
||||
this.ndvStore.draggableStopDragging();
|
||||
}, 0);
|
||||
},
|
||||
},
|
||||
});
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDragging.value && draggingElement.value) {
|
||||
isDragging.value = true;
|
||||
|
||||
const data = props.targetDataKey ? draggingElement.value.dataset.value : props.data ?? '';
|
||||
|
||||
ndvStore.draggableStartDragging({
|
||||
type: props.type,
|
||||
data: data ?? '',
|
||||
dimensions: draggingElement.value?.getBoundingClientRect() ?? null,
|
||||
});
|
||||
|
||||
emit('dragstart', draggingElement.value);
|
||||
document.body.style.cursor = 'grabbing';
|
||||
}
|
||||
|
||||
animationFrameId.value = window.requestAnimationFrame(() => {
|
||||
if (canDrop.value && stickyPosition.value) {
|
||||
draggablePosition.value = stickyPosition.value;
|
||||
} else {
|
||||
draggablePosition.value = [event.pageX, event.pageY];
|
||||
}
|
||||
emit('drag', draggablePosition.value);
|
||||
});
|
||||
};
|
||||
|
||||
const onDragEnd = () => {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.body.style.cursor = 'unset';
|
||||
window.removeEventListener('mousemove', onDrag);
|
||||
window.removeEventListener('mouseup', onDragEnd);
|
||||
if (isPresent(animationFrameId.value)) {
|
||||
window.cancelAnimationFrame(animationFrameId.value);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (draggingElement.value) emit('dragend', draggingElement.value);
|
||||
isDragging.value = false;
|
||||
draggingElement.value = undefined;
|
||||
ndvStore.draggableStopDragging();
|
||||
}, 0);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
@ -166,6 +143,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
.draggable {
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
z-index: 9999999;
|
||||
top: 0;
|
||||
|
|
|
@ -1,125 +1,85 @@
|
|||
<template>
|
||||
<div ref="target">
|
||||
<div ref="targetRef" @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" @mouseup="onMouseUp">
|
||||
<slot :droppable="droppable" :active-drop="activeDrop"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
<script setup lang="ts">
|
||||
import type { XYPosition } from '@/Interface';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import type { XYPosition } from '@/Interface';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
sticky: {
|
||||
type: Boolean,
|
||||
},
|
||||
stickyOffset: {
|
||||
type: Array as PropType<number[]>,
|
||||
default: () => [0, 0],
|
||||
},
|
||||
stickyOrigin: {
|
||||
type: String as PropType<'top-left' | 'center'>,
|
||||
default: 'top-left',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hovering: false,
|
||||
dimensions: null as DOMRect | null,
|
||||
id: uuid(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useNDVStore),
|
||||
isDragging(): boolean {
|
||||
return this.ndvStore.isDraggableDragging;
|
||||
},
|
||||
draggableType(): string {
|
||||
return this.ndvStore.draggableType;
|
||||
},
|
||||
draggableDimensions(): DOMRect | null {
|
||||
return this.ndvStore.draggable.dimensions;
|
||||
},
|
||||
droppable(): boolean {
|
||||
return !this.disabled && this.isDragging && this.draggableType === this.type;
|
||||
},
|
||||
activeDrop(): boolean {
|
||||
return this.droppable && this.hovering;
|
||||
},
|
||||
stickyPosition(): XYPosition | null {
|
||||
if (this.disabled || !this.sticky || !this.hovering || !this.dimensions) {
|
||||
return null;
|
||||
}
|
||||
type Props = {
|
||||
type: string;
|
||||
disabled?: boolean;
|
||||
sticky?: boolean;
|
||||
stickyOffset?: XYPosition;
|
||||
stickyOrigin?: 'top-left' | 'center';
|
||||
};
|
||||
|
||||
if (this.stickyOrigin === 'center') {
|
||||
return [
|
||||
this.dimensions.left +
|
||||
this.stickyOffset[0] +
|
||||
this.dimensions.width / 2 -
|
||||
(this.draggableDimensions?.width ?? 0) / 2,
|
||||
this.dimensions.top +
|
||||
this.stickyOffset[1] +
|
||||
this.dimensions.height / 2 -
|
||||
(this.draggableDimensions?.height ?? 0) / 2,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
this.dimensions.left + this.stickyOffset[0],
|
||||
this.dimensions.top + this.stickyOffset[1],
|
||||
];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
activeDrop(active) {
|
||||
if (active) {
|
||||
this.ndvStore.setDraggableTarget({ id: this.id, stickyPosition: this.stickyPosition });
|
||||
} else if (this.ndvStore.draggable.activeTarget?.id === this.id) {
|
||||
// Only clear active target if it is this one
|
||||
this.ndvStore.setDraggableTarget(null);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('mousemove', this.onMouseMove);
|
||||
window.addEventListener('mouseup', this.onMouseUp);
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
window.removeEventListener('mousemove', this.onMouseMove);
|
||||
window.removeEventListener('mouseup', this.onMouseUp);
|
||||
},
|
||||
methods: {
|
||||
onMouseMove(e: MouseEvent) {
|
||||
const targetRef = this.$refs.target as HTMLElement | undefined;
|
||||
|
||||
if (targetRef && this.isDragging) {
|
||||
const dim = targetRef.getBoundingClientRect();
|
||||
|
||||
this.dimensions = dim;
|
||||
this.hovering =
|
||||
e.clientX >= dim.left &&
|
||||
e.clientX <= dim.right &&
|
||||
e.clientY >= dim.top &&
|
||||
e.clientY <= dim.bottom;
|
||||
}
|
||||
},
|
||||
onMouseUp() {
|
||||
if (this.activeDrop) {
|
||||
const data = this.ndvStore.draggableData;
|
||||
this.$emit('drop', data);
|
||||
}
|
||||
},
|
||||
},
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
disabled: false,
|
||||
sticky: false,
|
||||
stickyOffset: () => [0, 0],
|
||||
stickyOrigin: 'top-left',
|
||||
});
|
||||
const emit = defineEmits<{
|
||||
(event: 'drop', value: string): void;
|
||||
}>();
|
||||
|
||||
const hovering = ref(false);
|
||||
const targetRef = ref<HTMLElement>();
|
||||
const id = ref(uuid());
|
||||
|
||||
const ndvStore = useNDVStore();
|
||||
const isDragging = computed(() => ndvStore.isDraggableDragging);
|
||||
const draggableType = computed(() => ndvStore.draggableType);
|
||||
const draggableDimensions = computed(() => ndvStore.draggable.dimensions);
|
||||
const droppable = computed(
|
||||
() => !props.disabled && isDragging.value && draggableType.value === props.type,
|
||||
);
|
||||
const activeDrop = computed(() => droppable.value && hovering.value);
|
||||
|
||||
watch(activeDrop, (active) => {
|
||||
if (active) {
|
||||
const stickyPosition = getStickyPosition();
|
||||
ndvStore.setDraggableTarget({ id: id.value, stickyPosition });
|
||||
} else if (ndvStore.draggable.activeTarget?.id === id.value) {
|
||||
// Only clear active target if it is this one
|
||||
ndvStore.setDraggableTarget(null);
|
||||
}
|
||||
});
|
||||
|
||||
function onMouseEnter() {
|
||||
hovering.value = true;
|
||||
}
|
||||
|
||||
function onMouseLeave() {
|
||||
hovering.value = false;
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
if (activeDrop.value) {
|
||||
const data = ndvStore.draggableData;
|
||||
emit('drop', data);
|
||||
}
|
||||
}
|
||||
|
||||
function getStickyPosition(): XYPosition | null {
|
||||
if (props.disabled || !props.sticky || !hovering.value || !targetRef.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { left, top, width, height } = targetRef.value.getBoundingClientRect();
|
||||
|
||||
if (props.stickyOrigin === 'center') {
|
||||
return [
|
||||
left + props.stickyOffset[0] + width / 2 - (draggableDimensions.value?.width ?? 0) / 2,
|
||||
top + props.stickyOffset[1] + height / 2 - (draggableDimensions.value?.height ?? 0) / 2,
|
||||
];
|
||||
}
|
||||
|
||||
return [left + props.stickyOffset[0], top + props.stickyOffset[1]];
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -56,6 +56,7 @@ import { useNDVStore } from '@/stores/ndv.store';
|
|||
import { ndvEventBus } from '@/event-bus';
|
||||
import NDVFloatingNodes from '@/components/NDVFloatingNodes.vue';
|
||||
import { useDebounce } from '@/composables/useDebounce';
|
||||
import type { XYPosition } from '@/Interface';
|
||||
|
||||
const SIDE_MARGIN = 24;
|
||||
const SIDE_PANELS_MARGIN = 80;
|
||||
|
@ -385,8 +386,9 @@ export default defineComponent({
|
|||
this.isDragging = true;
|
||||
this.$emit('dragstart', { position: this.mainPanelDimensions.relativeLeft });
|
||||
},
|
||||
onDrag(e: { x: number; y: number }) {
|
||||
const relativeLeft = this.pxToRelativeWidth(e.x) - this.mainPanelDimensions.relativeWidth / 2;
|
||||
onDrag(position: XYPosition) {
|
||||
const relativeLeft =
|
||||
this.pxToRelativeWidth(position[0]) - this.mainPanelDimensions.relativeWidth / 2;
|
||||
|
||||
this.setPositions(relativeLeft);
|
||||
},
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import Draggable from './Draggable.vue';
|
||||
import type { XYPosition } from '@/Interface';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
|
@ -58,7 +59,7 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
onDrag(e: { x: number; y: number }) {
|
||||
onDrag(e: XYPosition) {
|
||||
this.$emit('drag', e);
|
||||
},
|
||||
onDragStart() {
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
:data="getExpression(column)"
|
||||
:disabled="!mappingEnabled"
|
||||
@dragstart="onDragStart"
|
||||
@dragend="(column) => onDragEnd(column, 'column')"
|
||||
@dragend="(column) => onDragEnd(column?.textContent ?? '', 'column')"
|
||||
>
|
||||
<template #preview="{ canDrop }">
|
||||
<MappingPill :html="shorten(column, 16, 2)" :can-drop="canDrop" />
|
||||
|
@ -345,7 +345,7 @@ export default defineComponent({
|
|||
path: [column],
|
||||
});
|
||||
},
|
||||
getPathNameFromTarget(el: HTMLElement) {
|
||||
getPathNameFromTarget(el?: HTMLElement) {
|
||||
if (!el) {
|
||||
return '';
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue