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,
};
},
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;
}
this.draggingEl = e.target as HTMLElement; const isDragging = ref(false);
if (this.targetDataKey && this.draggingEl.dataset?.target !== this.targetDataKey) { const draggingElement = ref<HTMLElement>();
this.draggingEl = this.draggingEl.closest( const draggablePosition = ref<XYPosition>([0, 0]);
`[data-target="${this.targetDataKey}"]`, const animationFrameId = ref<number>();
) as HTMLElement; const ndvStore = useNDVStore();
}
if (this.targetDataKey && this.draggingEl?.dataset?.target !== this.targetDataKey) { const draggableStyle = computed<StyleValue>(() => ({
return; transform: `translate(${draggablePosition.value[0]}px, ${draggablePosition.value[1]}px)`,
} }));
e.preventDefault(); const canDrop = computed(() => ndvStore.canDraggableDrop);
e.stopPropagation();
this.isDragging = false; const stickyPosition = computed(() => ndvStore.draggableStickyPos);
this.draggablePosition = { x: e.pageX, y: e.pageY };
this.setDraggableStyle();
window.addEventListener('mousemove', this.onDrag); const onDragStart = (event: MouseEvent) => {
window.addEventListener('mouseup', this.onDragEnd); if (props.disabled) {
return;
}
// blur so that any focused inputs update value draggingElement.value = event.target as HTMLElement;
const activeElement = document.activeElement as HTMLElement; if (props.targetDataKey && draggingElement.value.dataset?.target !== props.targetDataKey) {
if (activeElement) { draggingElement.value = draggingElement.value.closest(
activeElement.blur(); `[data-target="${props.targetDataKey}"]`,
} ) as HTMLElement;
}, }
onDrag(e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
if (this.disabled) { if (props.targetDataKey && draggingElement.value?.dataset?.target !== props.targetDataKey) {
return; return;
} }
if (!this.isDragging) { event.preventDefault();
this.isDragging = true; event.stopPropagation();
const data = isDragging.value = false;
this.targetDataKey && this.draggingEl ? this.draggingEl.dataset.value : this.data || ''; draggablePosition.value = [event.pageX, event.pageY];
this.ndvStore.draggableStartDragging({
type: this.type,
data: data || '',
dimensions: this.draggingEl?.getBoundingClientRect() ?? null,
});
this.$emit('dragstart', this.draggingEl); window.addEventListener('mousemove', onDrag);
document.body.style.cursor = 'grabbing'; window.addEventListener('mouseup', onDragEnd);
}
this.animationFrameId = window.requestAnimationFrame(() => { // blur so that any focused inputs update value
if (this.canDrop && this.stickyPosition) { const activeElement = document.activeElement as HTMLElement;
this.draggablePosition = { x: this.stickyPosition[0], y: this.stickyPosition[1] }; if (activeElement) {
} else { activeElement.blur();
this.draggablePosition = { x: e.pageX, y: e.pageY }; }
} };
this.setDraggableStyle();
this.$emit('drag', this.draggablePosition);
});
},
onDragEnd() {
if (this.disabled) {
return;
}
document.body.style.cursor = 'unset'; const onDrag = (event: MouseEvent) => {
window.removeEventListener('mousemove', this.onDrag); event.preventDefault();
window.removeEventListener('mouseup', this.onDragEnd); event.stopPropagation();
window.cancelAnimationFrame(this.animationFrameId);
setTimeout(() => { if (props.disabled) {
this.$emit('dragend', this.draggingEl); return;
this.isDragging = false; }
this.draggingEl = null;
this.ndvStore.draggableStopDragging(); if (!isDragging.value && draggingElement.value) {
}, 0); 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> </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: {
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;
}
if (this.stickyOrigin === 'center') { const props = withDefaults(defineProps<Props>(), {
return [ disabled: false,
this.dimensions.left + sticky: false,
this.stickyOffset[0] + stickyOffset: () => [0, 0],
this.dimensions.width / 2 - stickyOrigin: 'top-left',
(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 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> </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 '';
} }