refactor(editor): Convert Draggable components to composition API (no-changelog) (#9889)

Co-authored-by: Ricardo Espinoza <ricardo@n8n.io>
This commit is contained in:
Elias Meire 2024-07-02 20:55:19 +02:00 committed by GitHub
parent 8debac755e
commit ae67d6b753
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 193 additions and 259 deletions

View file

@ -317,19 +317,12 @@ describe('Data mapping', () => {
workflowPage.actions.zoomToFit(); workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set'); 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="checkbox"]').should('exist');
ndv.getters.parameterInput('includeOtherFields').find('input[type="text"]').should('not.exist'); ndv.getters.parameterInput('includeOtherFields').find('input[type="text"]').should('not.exist');
ndv.getters const pill = ndv.getters.inputDataContainer().find('span').contains('count');
.inputDataContainer() pill.should('be.visible');
.should('exist') pill.realMouseDown();
.find('span') pill.realMouseMove(100, 100);
.contains('count')
.realMouseDown()
.realMouseMove(100, 100);
cy.wait(50);
ndv.getters ndv.getters
.parameterInput('includeOtherFields') .parameterInput('includeOtherFields')
@ -340,13 +333,13 @@ describe('Data mapping', () => {
.find('input[type="text"]') .find('input[type="text"]')
.should('exist') .should('exist')
.invoke('css', 'border') .invoke('css', 'border')
.then((border) => expect(border).to.include('dashed rgb(90, 76, 194)')); .should('include', 'dashed rgb(90, 76, 194)');
ndv.getters ndv.getters
.parameterInput('value') .parameterInput('value')
.find('input[type="text"]') .find('input[type="text"]')
.should('exist') .should('exist')
.invoke('css', 'border') .invoke('css', 'border')
.then((border) => expect(border).to.include('dashed rgb(90, 76, 194)')); .should('include', 'dashed rgb(90, 76, 194)');
}); });
}); });

View file

@ -101,7 +101,7 @@ describe('AssignmentCollection.vue', () => {
setActivePinia(pinia); setActivePinia(pinia);
const { getByTestId, findAllByTestId } = renderComponent({ 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: 'boolKey', value: true, dropArea });
await dropAssignment({ key: 'stringKey', value: 'stringValue', dropArea }); await dropAssignment({ key: 'stringKey', value: 'stringValue', dropArea });

View file

@ -9,154 +9,131 @@
<Teleport to="body"> <Teleport to="body">
<div v-show="isDragging" ref="draggable" :class="$style.draggable" :style="draggableStyle"> <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> </div>
</Teleport> </Teleport>
</component> </component>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import type { XYPosition } from '@/Interface'; import type { XYPosition } from '@/Interface';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { mapStores } from 'pinia'; import { isPresent } from '@/utils/typesUtils';
import { defineComponent } from 'vue'; import { type StyleValue, computed, ref } from 'vue';
export default defineComponent({ type Props = {
name: 'Draggable', type: string;
props: { data?: string;
disabled: { tag?: string;
type: Boolean, targetDataKey?: string;
}, disabled?: boolean;
type: { };
type: String,
required: true,
},
data: {
type: String,
},
tag: {
type: String,
default: 'div',
},
targetDataKey: {
type: String,
},
},
data() {
const draggablePosition = {
x: -100,
y: -100,
};
return { const props = withDefaults(defineProps<Props>(), { tag: 'div', disabled: false });
isDragging: false, const emit = defineEmits<{
draggablePosition, (event: 'drag', value: XYPosition): void;
draggingEl: null as null | HTMLElement, (event: 'dragstart', value: HTMLElement): void;
draggableStyle: { (event: 'dragend', value: HTMLElement): void;
transform: `translate(${draggablePosition.x}px, ${draggablePosition.y}px)`, }>();
},
animationFrameId: 0, const isDragging = ref(false);
}; const draggingElement = ref<HTMLElement>();
}, const draggablePosition = ref<XYPosition>([0, 0]);
computed: { const animationFrameId = ref<number>();
...mapStores(useNDVStore), const ndvStore = useNDVStore();
canDrop(): boolean {
return this.ndvStore.canDraggableDrop; const draggableStyle = computed<StyleValue>(() => ({
}, transform: `translate(${draggablePosition.value[0]}px, ${draggablePosition.value[1]}px)`,
stickyPosition(): XYPosition | null { }));
return this.ndvStore.draggableStickyPos;
}, const canDrop = computed(() => ndvStore.canDraggableDrop);
},
methods: { const stickyPosition = computed(() => ndvStore.draggableStickyPos);
setDraggableStyle() {
this.draggableStyle = { const onDragStart = (event: MouseEvent) => {
transform: `translate(${this.draggablePosition.x}px, ${this.draggablePosition.y}px)`, if (props.disabled) {
};
},
onDragStart(e: MouseEvent) {
if (this.disabled) {
return; return;
} }
this.draggingEl = e.target as HTMLElement; draggingElement.value = event.target as HTMLElement;
if (this.targetDataKey && this.draggingEl.dataset?.target !== this.targetDataKey) { if (props.targetDataKey && draggingElement.value.dataset?.target !== props.targetDataKey) {
this.draggingEl = this.draggingEl.closest( draggingElement.value = draggingElement.value.closest(
`[data-target="${this.targetDataKey}"]`, `[data-target="${props.targetDataKey}"]`,
) as HTMLElement; ) as HTMLElement;
} }
if (this.targetDataKey && this.draggingEl?.dataset?.target !== this.targetDataKey) { if (props.targetDataKey && draggingElement.value?.dataset?.target !== props.targetDataKey) {
return; return;
} }
e.preventDefault(); event.preventDefault();
e.stopPropagation(); event.stopPropagation();
this.isDragging = false; isDragging.value = false;
this.draggablePosition = { x: e.pageX, y: e.pageY }; draggablePosition.value = [event.pageX, event.pageY];
this.setDraggableStyle();
window.addEventListener('mousemove', this.onDrag); window.addEventListener('mousemove', onDrag);
window.addEventListener('mouseup', this.onDragEnd); window.addEventListener('mouseup', onDragEnd);
// blur so that any focused inputs update value // blur so that any focused inputs update value
const activeElement = document.activeElement as HTMLElement; const activeElement = document.activeElement as HTMLElement;
if (activeElement) { if (activeElement) {
activeElement.blur(); activeElement.blur();
} }
}, };
onDrag(e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
if (this.disabled) { const onDrag = (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
if (props.disabled) {
return; return;
} }
if (!this.isDragging) { if (!isDragging.value && draggingElement.value) {
this.isDragging = true; isDragging.value = true;
const data = const data = props.targetDataKey ? draggingElement.value.dataset.value : props.data ?? '';
this.targetDataKey && this.draggingEl ? this.draggingEl.dataset.value : this.data || '';
this.ndvStore.draggableStartDragging({ ndvStore.draggableStartDragging({
type: this.type, type: props.type,
data: data || '', data: data ?? '',
dimensions: this.draggingEl?.getBoundingClientRect() ?? null, dimensions: draggingElement.value?.getBoundingClientRect() ?? null,
}); });
this.$emit('dragstart', this.draggingEl); emit('dragstart', draggingElement.value);
document.body.style.cursor = 'grabbing'; document.body.style.cursor = 'grabbing';
} }
this.animationFrameId = window.requestAnimationFrame(() => { animationFrameId.value = window.requestAnimationFrame(() => {
if (this.canDrop && this.stickyPosition) { if (canDrop.value && stickyPosition.value) {
this.draggablePosition = { x: this.stickyPosition[0], y: this.stickyPosition[1] }; draggablePosition.value = stickyPosition.value;
} else { } else {
this.draggablePosition = { x: e.pageX, y: e.pageY }; draggablePosition.value = [event.pageX, event.pageY];
} }
this.setDraggableStyle(); emit('drag', draggablePosition.value);
this.$emit('drag', this.draggablePosition);
}); });
}, };
onDragEnd() {
if (this.disabled) { const onDragEnd = () => {
if (props.disabled) {
return; return;
} }
document.body.style.cursor = 'unset'; document.body.style.cursor = 'unset';
window.removeEventListener('mousemove', this.onDrag); window.removeEventListener('mousemove', onDrag);
window.removeEventListener('mouseup', this.onDragEnd); window.removeEventListener('mouseup', onDragEnd);
window.cancelAnimationFrame(this.animationFrameId); if (isPresent(animationFrameId.value)) {
window.cancelAnimationFrame(animationFrameId.value);
}
setTimeout(() => { setTimeout(() => {
this.$emit('dragend', this.draggingEl); if (draggingElement.value) emit('dragend', draggingElement.value);
this.isDragging = false; isDragging.value = false;
this.draggingEl = null; draggingElement.value = undefined;
this.ndvStore.draggableStopDragging(); ndvStore.draggableStopDragging();
}, 0); }, 0);
}, };
},
});
</script> </script>
<style lang="scss" module> <style lang="scss" module>
@ -166,6 +143,7 @@ export default defineComponent({
} }
.draggable { .draggable {
pointer-events: none;
position: fixed; position: fixed;
z-index: 9999999; z-index: 9999999;
top: 0; top: 0;

View file

@ -1,125 +1,85 @@
<template> <template>
<div ref="target"> <div ref="targetRef" @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" @mouseup="onMouseUp">
<slot :droppable="droppable" :active-drop="activeDrop"></slot> <slot :droppable="droppable" :active-drop="activeDrop"></slot>
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue'; import type { XYPosition } from '@/Interface';
import type { PropType } from 'vue';
import { mapStores } from 'pinia';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import type { XYPosition } from '@/Interface'; import { computed, ref, watch } from 'vue';
export default defineComponent({ type Props = {
props: { type: string;
type: { disabled?: boolean;
type: String, sticky?: boolean;
}, stickyOffset?: XYPosition;
disabled: { stickyOrigin?: 'top-left' | 'center';
type: Boolean, };
},
sticky: { const props = withDefaults(defineProps<Props>(), {
type: Boolean, disabled: false,
}, sticky: false,
stickyOffset: { stickyOffset: () => [0, 0],
type: Array as PropType<number[]>, stickyOrigin: 'top-left',
default: () => [0, 0], });
}, const emit = defineEmits<{
stickyOrigin: { (event: 'drop', value: string): void;
type: String as PropType<'top-left' | 'center'>, }>();
default: 'top-left',
}, const hovering = ref(false);
}, const targetRef = ref<HTMLElement>();
data() { const id = ref(uuid());
return {
hovering: false, const ndvStore = useNDVStore();
dimensions: null as DOMRect | null, const isDragging = computed(() => ndvStore.isDraggableDragging);
id: uuid(), const draggableType = computed(() => ndvStore.draggableType);
}; const draggableDimensions = computed(() => ndvStore.draggable.dimensions);
}, const droppable = computed(
computed: { () => !props.disabled && isDragging.value && draggableType.value === props.type,
...mapStores(useNDVStore), );
isDragging(): boolean { const activeDrop = computed(() => droppable.value && hovering.value);
return this.ndvStore.isDraggableDragging;
}, watch(activeDrop, (active) => {
draggableType(): string { if (active) {
return this.ndvStore.draggableType; const stickyPosition = getStickyPosition();
}, ndvStore.setDraggableTarget({ id: id.value, stickyPosition });
draggableDimensions(): DOMRect | null { } else if (ndvStore.draggable.activeTarget?.id === id.value) {
return this.ndvStore.draggable.dimensions; // Only clear active target if it is this one
}, ndvStore.setDraggableTarget(null);
droppable(): boolean { }
return !this.disabled && this.isDragging && this.draggableType === this.type; });
},
activeDrop(): boolean { function onMouseEnter() {
return this.droppable && this.hovering; hovering.value = true;
}, }
stickyPosition(): XYPosition | null {
if (this.disabled || !this.sticky || !this.hovering || !this.dimensions) { 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; return null;
} }
if (this.stickyOrigin === 'center') { const { left, top, width, height } = targetRef.value.getBoundingClientRect();
if (props.stickyOrigin === 'center') {
return [ return [
this.dimensions.left + left + props.stickyOffset[0] + width / 2 - (draggableDimensions.value?.width ?? 0) / 2,
this.stickyOffset[0] + top + props.stickyOffset[1] + height / 2 - (draggableDimensions.value?.height ?? 0) / 2,
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 [ return [left + props.stickyOffset[0], top + props.stickyOffset[1]];
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);
}
},
},
});
</script> </script>

View file

@ -56,6 +56,7 @@ import { useNDVStore } from '@/stores/ndv.store';
import { ndvEventBus } from '@/event-bus'; import { ndvEventBus } from '@/event-bus';
import NDVFloatingNodes from '@/components/NDVFloatingNodes.vue'; import NDVFloatingNodes from '@/components/NDVFloatingNodes.vue';
import { useDebounce } from '@/composables/useDebounce'; import { useDebounce } from '@/composables/useDebounce';
import type { XYPosition } from '@/Interface';
const SIDE_MARGIN = 24; const SIDE_MARGIN = 24;
const SIDE_PANELS_MARGIN = 80; const SIDE_PANELS_MARGIN = 80;
@ -385,8 +386,9 @@ export default defineComponent({
this.isDragging = true; this.isDragging = true;
this.$emit('dragstart', { position: this.mainPanelDimensions.relativeLeft }); this.$emit('dragstart', { position: this.mainPanelDimensions.relativeLeft });
}, },
onDrag(e: { x: number; y: number }) { onDrag(position: XYPosition) {
const relativeLeft = this.pxToRelativeWidth(e.x) - this.mainPanelDimensions.relativeWidth / 2; const relativeLeft =
this.pxToRelativeWidth(position[0]) - this.mainPanelDimensions.relativeWidth / 2;
this.setPositions(relativeLeft); this.setPositions(relativeLeft);
}, },

View file

@ -44,6 +44,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import Draggable from './Draggable.vue'; import Draggable from './Draggable.vue';
import type { XYPosition } from '@/Interface';
export default defineComponent({ export default defineComponent({
components: { components: {
@ -58,7 +59,7 @@ export default defineComponent({
}, },
}, },
methods: { methods: {
onDrag(e: { x: number; y: number }) { onDrag(e: XYPosition) {
this.$emit('drag', e); this.$emit('drag', e);
}, },
onDragStart() { onDragStart() {

View file

@ -37,7 +37,7 @@
:data="getExpression(column)" :data="getExpression(column)"
:disabled="!mappingEnabled" :disabled="!mappingEnabled"
@dragstart="onDragStart" @dragstart="onDragStart"
@dragend="(column) => onDragEnd(column, 'column')" @dragend="(column) => onDragEnd(column?.textContent ?? '', 'column')"
> >
<template #preview="{ canDrop }"> <template #preview="{ canDrop }">
<MappingPill :html="shorten(column, 16, 2)" :can-drop="canDrop" /> <MappingPill :html="shorten(column, 16, 2)" :can-drop="canDrop" />
@ -345,7 +345,7 @@ export default defineComponent({
path: [column], path: [column],
}); });
}, },
getPathNameFromTarget(el: HTMLElement) { getPathNameFromTarget(el?: HTMLElement) {
if (!el) { if (!el) {
return ''; return '';
} }