n8n/packages/editor-ui/src/components/NDVDraggablePanels.vue
कारतोफ्फेलस्क्रिप्ट™ 372d5c7d01
ci: Upgrade eslint, prettier, typescript, and some other dev tooling (no-changelog) (#8895)
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
2024-03-26 14:22:57 +01:00

494 lines
14 KiB
Vue

<template>
<div>
<NDVFloatingNodes
v-if="activeNode"
:root-node="activeNode"
@switch-selected-node="onSwitchSelectedNode"
/>
<div v-if="!hideInputAndOutput" :class="$style.inputPanel" :style="inputPanelStyles">
<slot name="input"></slot>
</div>
<div v-if="!hideInputAndOutput" :class="$style.outputPanel" :style="outputPanelStyles">
<slot name="output"></slot>
</div>
<div :class="$style.mainPanel" :style="mainPanelStyles">
<n8n-resize-wrapper
:is-resizing-enabled="currentNodePaneType !== 'unknown'"
:width="relativeWidthToPx(mainPanelDimensions.relativeWidth)"
:min-width="MIN_PANEL_WIDTH"
:grid-size="20"
:supported-directions="supportedResizeDirections"
@resize="onResizeDebounced"
@resizestart="onResizeStart"
@resizeend="onResizeEnd"
>
<div :class="$style.dragButtonContainer">
<PanelDragButton
v-if="!hideInputAndOutput && isDraggable"
:class="{ [$style.draggable]: true, [$style.visible]: isDragging }"
:can-move-left="canMoveLeft"
:can-move-right="canMoveRight"
@dragstart="onDragStart"
@drag="onDrag"
@dragend="onDragEnd"
/>
</div>
<div :class="{ [$style.mainPanelInner]: true, [$style.dragging]: isDragging }">
<slot name="main" />
</div>
</n8n-resize-wrapper>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import type { PropType } from 'vue';
import { mapStores } from 'pinia';
import { get } from 'lodash-es';
import { useStorage } from '@/composables/useStorage';
import type { INodeTypeDescription } from 'n8n-workflow';
import PanelDragButton from './PanelDragButton.vue';
import { LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH, MAIN_NODE_PANEL_WIDTH } from '@/constants';
import { useNDVStore } from '@/stores/ndv.store';
import { ndvEventBus } from '@/event-bus';
import NDVFloatingNodes from '@/components/NDVFloatingNodes.vue';
import { useDebounce } from '@/composables/useDebounce';
const SIDE_MARGIN = 24;
const SIDE_PANELS_MARGIN = 80;
const MIN_PANEL_WIDTH = 280;
const PANEL_WIDTH = 320;
const PANEL_WIDTH_LARGE = 420;
const initialMainPanelWidth: { [key: string]: number } = {
regular: MAIN_NODE_PANEL_WIDTH,
dragless: MAIN_NODE_PANEL_WIDTH,
unknown: MAIN_NODE_PANEL_WIDTH,
inputless: MAIN_NODE_PANEL_WIDTH,
wide: MAIN_NODE_PANEL_WIDTH * 2,
};
export default defineComponent({
name: 'NDVDraggablePanels',
components: {
PanelDragButton,
NDVFloatingNodes,
},
props: {
isDraggable: {
type: Boolean,
},
hideInputAndOutput: {
type: Boolean,
},
position: {
type: Number,
},
nodeType: {
type: Object as PropType<INodeTypeDescription>,
default: () => ({}),
},
},
setup() {
const { callDebounced } = useDebounce();
return { callDebounced };
},
data(): {
windowWidth: number;
isDragging: boolean;
MIN_PANEL_WIDTH: number;
initialized: boolean;
} {
return {
windowWidth: 1,
isDragging: false,
MIN_PANEL_WIDTH,
initialized: false,
};
},
mounted() {
this.setTotalWidth();
/*
Only set(or restore) initial position if `mainPanelDimensions`
is at the default state({relativeLeft:1, relativeRight: 1, relativeWidth: 1}) to make sure we use store values if they are set
*/
if (
this.mainPanelDimensions.relativeLeft === 1 &&
this.mainPanelDimensions.relativeRight === 1
) {
this.setMainPanelWidth();
this.setPositions(this.getInitialLeftPosition(this.mainPanelDimensions.relativeWidth));
this.restorePositionData();
}
window.addEventListener('resize', this.setTotalWidth);
this.$emit('init', { position: this.mainPanelDimensions.relativeLeft });
setTimeout(() => {
this.initialized = true;
}, 0);
ndvEventBus.on('setPositionByName', this.setPositionByName);
},
beforeUnmount() {
window.removeEventListener('resize', this.setTotalWidth);
ndvEventBus.off('setPositionByName', this.setPositionByName);
},
computed: {
...mapStores(useNDVStore),
mainPanelDimensions(): {
relativeWidth: number;
relativeLeft: number;
relativeRight: number;
} {
return this.ndvStore.getMainPanelDimensions(this.currentNodePaneType);
},
activeNode() {
return this.ndvStore.activeNode;
},
supportedResizeDirections(): string[] {
const supportedDirections = ['right'];
if (this.isDraggable) supportedDirections.push('left');
return supportedDirections;
},
currentNodePaneType(): string {
if (!this.hasInputSlot) return 'inputless';
if (!this.isDraggable) return 'dragless';
if (this.nodeType === null) return 'unknown';
return get(this, 'nodeType.parameterPane') || 'regular';
},
hasInputSlot(): boolean {
return this.$slots.input !== undefined;
},
inputPanelMargin(): number {
return this.pxToRelativeWidth(SIDE_PANELS_MARGIN);
},
minWindowWidth(): number {
return 2 * (SIDE_MARGIN + SIDE_PANELS_MARGIN) + MIN_PANEL_WIDTH;
},
minimumLeftPosition(): number {
if (this.windowWidth < this.minWindowWidth) return this.pxToRelativeWidth(1);
if (!this.hasInputSlot) return this.pxToRelativeWidth(SIDE_MARGIN);
return this.pxToRelativeWidth(SIDE_MARGIN + 20) + this.inputPanelMargin;
},
maximumRightPosition(): number {
if (this.windowWidth < this.minWindowWidth) return this.pxToRelativeWidth(1);
return this.pxToRelativeWidth(SIDE_MARGIN + 20) + this.inputPanelMargin;
},
canMoveLeft(): boolean {
return this.mainPanelDimensions.relativeLeft > this.minimumLeftPosition;
},
canMoveRight(): boolean {
return this.mainPanelDimensions.relativeRight > this.maximumRightPosition;
},
mainPanelStyles(): { left: string; right: string } {
return {
left: `${this.relativeWidthToPx(this.mainPanelDimensions.relativeLeft)}px`,
right: `${this.relativeWidthToPx(this.mainPanelDimensions.relativeRight)}px`,
};
},
inputPanelStyles(): { right: string } {
return {
right: `${this.relativeWidthToPx(this.calculatedPositions.inputPanelRelativeRight)}px`,
};
},
outputPanelStyles(): { left: string; transform: string } {
return {
left: `${this.relativeWidthToPx(this.calculatedPositions.outputPanelRelativeLeft)}px`,
transform: `translateX(-${this.relativeWidthToPx(this.outputPanelRelativeTranslate)}px)`,
};
},
calculatedPositions(): { inputPanelRelativeRight: number; outputPanelRelativeLeft: number } {
const hasInput = this.$slots.input !== undefined;
const outputPanelRelativeLeft =
this.mainPanelDimensions.relativeLeft + this.mainPanelDimensions.relativeWidth;
const inputPanelRelativeRight = hasInput
? 1 - outputPanelRelativeLeft + this.mainPanelDimensions.relativeWidth
: 1 - this.pxToRelativeWidth(SIDE_MARGIN);
return {
inputPanelRelativeRight,
outputPanelRelativeLeft,
};
},
outputPanelRelativeTranslate(): number {
const panelMinLeft = 1 - this.pxToRelativeWidth(MIN_PANEL_WIDTH + SIDE_MARGIN);
const currentRelativeLeftDelta =
this.calculatedPositions.outputPanelRelativeLeft - panelMinLeft;
return currentRelativeLeftDelta > 0 ? currentRelativeLeftDelta : 0;
},
hasDoubleWidth(): boolean {
return get(this, 'nodeType.parameterPane') === 'wide';
},
fixedPanelWidth(): number {
const multiplier = this.hasDoubleWidth ? 2 : 1;
if (this.windowWidth > 1700) {
return PANEL_WIDTH_LARGE * multiplier;
}
return PANEL_WIDTH * multiplier;
},
isBelowMinWidthMainPanel(): boolean {
const minRelativeWidth = this.pxToRelativeWidth(MIN_PANEL_WIDTH);
return this.mainPanelDimensions.relativeWidth < minRelativeWidth;
},
},
watch: {
windowWidth(windowWidth) {
const minRelativeWidth = this.pxToRelativeWidth(MIN_PANEL_WIDTH);
// Prevent the panel resizing below MIN_PANEL_WIDTH whhile maintaing position
if (this.isBelowMinWidthMainPanel) {
this.setMainPanelWidth(minRelativeWidth);
}
const isBelowMinLeft = this.minimumLeftPosition > this.mainPanelDimensions.relativeLeft;
const isMaxRight = this.maximumRightPosition > this.mainPanelDimensions.relativeRight;
// When user is resizing from non-supported view(sub ~488px) we need to refit the panels
if (windowWidth > this.minWindowWidth && isBelowMinLeft && isMaxRight) {
this.setMainPanelWidth(minRelativeWidth);
this.setPositions(this.getInitialLeftPosition(this.mainPanelDimensions.relativeWidth));
}
this.setPositions(this.mainPanelDimensions.relativeLeft);
},
},
methods: {
onSwitchSelectedNode(node: string) {
this.$emit('switchSelectedNode', node);
},
getInitialLeftPosition(width: number) {
if (this.currentNodePaneType === 'dragless')
return this.pxToRelativeWidth(SIDE_MARGIN + 1 + this.fixedPanelWidth);
return this.hasInputSlot ? 0.5 - width / 2 : this.minimumLeftPosition;
},
setMainPanelWidth(relativeWidth?: number) {
const mainPanelRelativeWidth =
relativeWidth || this.pxToRelativeWidth(initialMainPanelWidth[this.currentNodePaneType]);
this.ndvStore.setMainPanelDimensions({
panelType: this.currentNodePaneType,
dimensions: {
relativeWidth: mainPanelRelativeWidth,
},
});
},
setPositions(relativeLeft: number) {
const mainPanelRelativeLeft =
relativeLeft || 1 - this.calculatedPositions.inputPanelRelativeRight;
const mainPanelRelativeRight =
1 - mainPanelRelativeLeft - this.mainPanelDimensions.relativeWidth;
const isMaxRight = this.maximumRightPosition > mainPanelRelativeRight;
const isMinLeft = this.minimumLeftPosition > mainPanelRelativeLeft;
const isInputless = this.currentNodePaneType === 'inputless';
if (isMinLeft) {
this.ndvStore.setMainPanelDimensions({
panelType: this.currentNodePaneType,
dimensions: {
relativeLeft: this.minimumLeftPosition,
relativeRight: 1 - this.mainPanelDimensions.relativeWidth - this.minimumLeftPosition,
},
});
return;
}
if (isMaxRight) {
this.ndvStore.setMainPanelDimensions({
panelType: this.currentNodePaneType,
dimensions: {
relativeLeft: 1 - this.mainPanelDimensions.relativeWidth - this.maximumRightPosition,
relativeRight: this.maximumRightPosition,
},
});
return;
}
this.ndvStore.setMainPanelDimensions({
panelType: this.currentNodePaneType,
dimensions: {
relativeLeft: isInputless ? this.minimumLeftPosition : mainPanelRelativeLeft,
relativeRight: mainPanelRelativeRight,
},
});
},
setPositionByName(position: 'minLeft' | 'maxRight' | 'initial') {
const positionByName: Record<string, number> = {
minLeft: this.minimumLeftPosition,
maxRight: this.maximumRightPosition,
initial: this.getInitialLeftPosition(this.mainPanelDimensions.relativeWidth),
};
this.setPositions(positionByName[position]);
},
pxToRelativeWidth(px: number) {
return px / this.windowWidth;
},
relativeWidthToPx(relativeWidth: number) {
return relativeWidth * this.windowWidth;
},
onResizeStart() {
this.setTotalWidth();
},
onResizeEnd() {
this.storePositionData();
},
onResizeDebounced(data: { direction: string; x: number; width: number }) {
if (this.initialized) {
void this.callDebounced(this.onResize, { debounceTime: 10, trailing: true }, data);
}
},
onResize({ direction, x, width }: { direction: string; x: number; width: number }) {
const relativeDistance = this.pxToRelativeWidth(x);
const relativeWidth = this.pxToRelativeWidth(width);
if (direction === 'left' && relativeDistance <= this.minimumLeftPosition) return;
if (direction === 'right' && 1 - relativeDistance <= this.maximumRightPosition) return;
if (width <= MIN_PANEL_WIDTH) return;
this.setMainPanelWidth(relativeWidth);
this.setPositions(
direction === 'left' ? relativeDistance : this.mainPanelDimensions.relativeLeft,
);
},
restorePositionData() {
const storedPanelWidthData = useStorage(
`${LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH}_${this.currentNodePaneType}`,
).value;
if (storedPanelWidthData) {
const parsedWidth = parseFloat(storedPanelWidthData);
this.setMainPanelWidth(parsedWidth);
const initialPosition = this.getInitialLeftPosition(parsedWidth);
this.setPositions(initialPosition);
return true;
}
return false;
},
storePositionData() {
useStorage(`${LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH}_${this.currentNodePaneType}`).value =
this.mainPanelDimensions.relativeWidth.toString();
},
onDragStart() {
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;
this.setPositions(relativeLeft);
},
onDragEnd() {
setTimeout(() => {
this.isDragging = false;
this.$emit('dragend', {
windowWidth: this.windowWidth,
position: this.mainPanelDimensions.relativeLeft,
});
}, 0);
this.storePositionData();
},
setTotalWidth() {
this.windowWidth = window.innerWidth;
},
close() {
this.$emit('close');
},
},
});
</script>
<style lang="scss" module>
.dataPanel {
position: absolute;
height: calc(100% - 2 * var(--spacing-l));
position: absolute;
top: var(--spacing-l);
z-index: 0;
min-width: 280px;
}
.inputPanel {
composes: dataPanel;
left: var(--spacing-l);
> * {
border-radius: var(--border-radius-large) 0 0 var(--border-radius-large);
}
}
.outputPanel {
composes: dataPanel;
right: var(--spacing-l);
> * {
border-radius: 0 var(--border-radius-large) var(--border-radius-large) 0;
}
}
.mainPanel {
position: absolute;
height: 100%;
&:hover {
.draggable {
visibility: visible;
}
}
}
.mainPanelInner {
height: 100%;
border: var(--border-base);
border-radius: var(--border-radius-large);
box-shadow: 0 4px 16px rgb(50 61 85 / 10%);
overflow: hidden;
&.dragging {
border-color: var(--color-primary);
box-shadow: 0px 6px 16px rgba(255, 74, 51, 0.15);
}
}
.draggable {
visibility: hidden;
}
.double-width {
left: 90%;
}
.dragButtonContainer {
position: absolute;
top: -12px;
width: 100%;
height: 12px;
display: flex;
justify-content: center;
pointer-events: none;
.draggable {
pointer-events: all;
}
&:hover .draggable {
visibility: visible;
}
}
.visible {
visibility: visible;
}
</style>