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.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)');
});
});

View file

@ -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 });

View file

@ -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;

View file

@ -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>

View file

@ -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);
},

View file

@ -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() {

View file

@ -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 '';
}