refactor: back to jsx 🥇

This commit is contained in:
Braks
2021-08-08 19:28:45 +02:00
parent a6f7678c0f
commit 1d542aba65
31 changed files with 666 additions and 558 deletions
@@ -14,7 +14,7 @@
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import { ConnectionLineComponentProps } from '../../src'; import { ConnectionLineComponentProps } from '../../src';
const ConnectionLine = defineComponent({ export default defineComponent({
props: { props: {
sourceX: { sourceX: {
type: Number as PropType<ConnectionLineComponentProps['sourceX']>, type: Number as PropType<ConnectionLineComponentProps['sourceX']>,
@@ -34,6 +34,4 @@ const ConnectionLine = defineComponent({
} }
} }
}); });
export default ConnectionLine;
</script> </script>
@@ -1,9 +1,9 @@
<template> <template>
<RevueFlow <RevueFlow
v-model="elements" v-model="elements"
:connectionLineComponent="ConnectionLine" :connection-line-component="ConnectionLine"
:onElementsRemove="onElementsRemove" @elementsRemove="onElementsRemove"
:onConnect="onConnect" @connect="onConnect"
> >
<Background :variant="bgVariant" /> <Background :variant="bgVariant" />
</RevueFlow> </RevueFlow>
@@ -14,10 +14,17 @@ import RevueFlow, { removeElements, addEdge, Background, BackgroundVariant, Elem
import ConnectionLine from './ConnectionLine.vue'; import ConnectionLine from './ConnectionLine.vue';
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
const ConnectionLineFlow = defineComponent({ export default defineComponent({
components: { RevueFlow, Background }, components: { RevueFlow, Background },
setup() { setup() {
const elements = ref<Elements>([{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }]); const elements = ref<Elements>([
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 }
}
] as Elements);
const onElementsRemove = (elementsToRemove: Elements) => const onElementsRemove = (elementsToRemove: Elements) =>
(elements.value = removeElements(elementsToRemove, elements.value as Elements)); (elements.value = removeElements(elementsToRemove, elements.value as Elements));
const onConnect = (params: Connection | Edge) => (elements.value = addEdge(params, elements.value as Elements)); const onConnect = (params: Connection | Edge) => (elements.value = addEdge(params, elements.value as Elements));
@@ -31,6 +38,4 @@ const ConnectionLineFlow = defineComponent({
}; };
} }
}); });
export default ConnectionLineFlow;
</script> </script>
+1 -1
View File
@@ -116,7 +116,7 @@ const CustomNodeFlow = defineComponent({
{ id: 'e1-2', source: '1', target: '2', animated: true, style: { stroke: '#fff' } }, { id: 'e1-2', source: '1', target: '2', animated: true, style: { stroke: '#fff' } },
{ id: 'e2a-3', source: '2', sourceHandle: 'a', target: '3', animated: true, style: { stroke: '#fff' } }, { id: 'e2a-3', source: '2', sourceHandle: 'a', target: '3', animated: true, style: { stroke: '#fff' } },
{ id: 'e2b-4', source: '2', sourceHandle: 'b', target: '4', animated: true, style: { stroke: '#fff' } } { id: 'e2b-4', source: '2', sourceHandle: 'b', target: '4', animated: true, style: { stroke: '#fff' } }
]; ] as Elements;
}); });
const onElementsRemove = (elementsToRemove: Elements) => const onElementsRemove = (elementsToRemove: Elements) =>
+4 -11
View File
@@ -1,14 +1,7 @@
<template> <template>
<div class="dndflow"> <div class="dndflow">
<div class="revueflow-wrapper" @drop="onDrop"> <div class="revueflow-wrapper" @drop="onDrop">
<RevueFlow <RevueFlow v-model="elements" @elementsRemove="onElementsRemove" @load="onLoad" @connect="onConnect" @dragover="onDragOver">
v-model="elements"
:onConnect="onConnect"
:onElementsRemove="onElementsRemove"
:onLoad="onLoad"
@dragover="onDragOver"
@click="onDrop"
>
<Controls /> <Controls />
</RevueFlow> </RevueFlow>
</div> </div>
@@ -43,7 +36,7 @@ const DnDFlow = defineComponent({
data: { label: 'input node' }, data: { label: 'input node' },
position: { x: 250, y: 5 } position: { x: 250, y: 5 }
} }
]); ] as Elements);
let id = 0; let id = 0;
const getId = (): ElementId => `dndnode_${id++}`; const getId = (): ElementId => `dndnode_${id++}`;
@@ -67,12 +60,12 @@ const DnDFlow = defineComponent({
console.log(event.dataTransfer?.getData('application/revueflow')); console.log(event.dataTransfer?.getData('application/revueflow'));
const type = event.dataTransfer?.getData('application/revueflow'); const type = event.dataTransfer?.getData('application/revueflow');
const position = revueFlowInstance.value.project({ x: event.clientX, y: event.clientY - 40 }); const position = revueFlowInstance.value.project({ x: event.clientX, y: event.clientY - 40 });
const newNode: Node = { const newNode = {
id: getId(), id: getId(),
type, type,
position, position,
data: { label: `${type} node` } data: { label: `${type} node` }
}; } as Node;
elements.value.push(newNode); elements.value.push(newNode);
} }
+1 -1
View File
@@ -28,7 +28,7 @@
"lint": "yarn lint:js" "lint": "yarn lint:js"
}, },
"dependencies": { "dependencies": {
"@braks/revue-draggable": "^0.2.4", "@braks/revue-draggable": "^0.2.5",
"@types/d3": "^7.0.0", "@types/d3": "^7.0.0",
"@vueuse/core": "^5.1.4", "@vueuse/core": "^5.1.4",
"d3": "^7.0.0", "d3": "^7.0.0",
+1 -1
View File
@@ -90,7 +90,7 @@ const Controls = defineComponent({
const store = inject<RevueFlowStore>('store')!; const store = inject<RevueFlowStore>('store')!;
const isVisible = ref<boolean>(false); const isVisible = ref<boolean>(false);
const zoomHelper = ref<ZoomPanHelperFunctions>(); const zoomHelper = ref<ZoomPanHelperFunctions>();
const { onReady } = useZoomPanHelper(store); const { onReady } = useZoomPanHelper();
onReady((helper) => { onReady((helper) => {
zoomHelper.value = helper; zoomHelper.value = helper;
@@ -1,23 +1,4 @@
<template> import { ref, defineComponent, CSSProperties, PropType, computed, inject, h } from 'vue';
<g v-show="sourceNode && nodesConnectable" class="revue-flow__connection">
<g v-if="CustomConnectionLineComponent" class="revue-flow__connection">
<component
:is="CustomConnectionLineComponent"
:sourceX="sourceX"
:sourceY="sourceY"
:sourcePosition="sourceHandle.position"
:targetX="targetX"
:targetY="targetY"
:targetPosition="targetPosition"
:connectionLineType="connectionLineType"
:connectionLineStyle="connectionLineStyle"
/>
</g>
<path v-else class="revue-flow__connection-path" :d="dAttr" :style="connectionLineStyle" />
</g>
</template>
<script lang="ts">
import { ref, defineComponent, CSSProperties, PropType, computed, inject } from 'vue';
import { getBezierPath } from '../Edges/BezierEdge'; import { getBezierPath } from '../Edges/BezierEdge';
import { getSmoothStepPath } from '../Edges/SmoothStepEdge'; import { getSmoothStepPath } from '../Edges/SmoothStepEdge';
@@ -26,23 +7,23 @@ import { Node, HandleElement, Position, ConnectionLineType, ConnectionLineCompon
interface ConnectionLineProps { interface ConnectionLineProps {
connectionLineType: ConnectionLineType; connectionLineType: ConnectionLineType;
connectionLineStyle?: CSSProperties; connectionLineStyle?: CSSProperties;
CustomConnectionLineComponent?: ConnectionLineComponent; customConnectionLine?: ConnectionLineComponent;
} }
const ConnectionLine = defineComponent({ export default defineComponent({
props: { props: {
connectionLineType: {
type: String as PropType<ConnectionLineProps['connectionLineType']>,
required: false,
default: ConnectionLineType.Bezier
},
connectionLineStyle: { connectionLineStyle: {
type: Object as PropType<ConnectionLineProps['connectionLineStyle']>, type: Object as PropType<ConnectionLineProps['connectionLineStyle']>,
required: false, required: false,
default: () => {} default: () => {}
}, },
CustomConnectionLineComponent: { connectionLineType: {
type: Object as PropType<ConnectionLineProps['CustomConnectionLineComponent']>, type: String as PropType<ConnectionLineProps['connectionLineType']>,
required: false,
default: ConnectionLineType.Bezier
},
customConnectionLine: {
type: Object as PropType<ConnectionLineProps['customConnectionLine']>,
required: false, required: false,
default: undefined default: undefined
} }
@@ -114,20 +95,31 @@ const ConnectionLine = defineComponent({
); );
} }
return { if (props.customConnectionLine) {
dAttr, return () => (
targetX, <g class="revue-flow__connection">
targetY, {props.customConnectionLine &&
sourceX, h(props.customConnectionLine, {
sourceY, sourceX: sourceX.value,
sourceHandle, sourceY: sourceY.value,
sourceNode, sourcePosition: sourceHandle.value?.position,
sourceHandleX, targetX: targetX.value,
sourceHandleY, targetY: targetY.value,
nodesConnectable targetPosition: targetPosition.value,
}; connectionLineType: props.connectionLineType,
connectionLineStyle: props.connectionLineStyle
})}
</g>
);
}
return () =>
nodesConnectable.value && sourceNode.value ? (
<g class="revue-flow__connection">
<path d={dAttr.value} class="revue-flow__connection-path" style={props.connectionLineStyle} />
</g>
) : (
''
);
} }
}); });
export default ConnectionLine;
</script>
+1 -1
View File
@@ -121,7 +121,7 @@ const BezierEdge = defineComponent({
style: { style: {
type: Object as PropType<CSSProperties>, type: Object as PropType<CSSProperties>,
required: false, required: false,
default: () => ({}) default: () => {}
} }
}, },
setup(props) { setup(props) {
+255
View File
@@ -0,0 +1,255 @@
import { EdgeAnchor } from './EdgeAnchor';
import { ConnectionMode, Edge, Elements, Node, Position, RevueFlowStore, Transform } from '../../types';
import { computed, defineComponent, h, inject, PropType, ref } from 'vue';
import { RevueFlowHooks } from '../../hooks/RevueFlowHooks';
import { getEdgePositions, getHandle, getSourceTargetNodes, isEdgeVisible } from '../../container/EdgeRenderer/utils';
import { isEdge } from '../../utils/graph';
import { onMouseDown } from '../Handle/handler';
interface EdgeProps {
edge: Edge;
nodes: Node[];
selectedElements: Elements | null;
elementsSelectable: boolean;
transform: Transform;
width: number;
height: number;
onlyRenderVisibleElements: boolean;
connectionMode?: ConnectionMode;
markerEndId?: string;
edgeUpdaterRadius?: number;
}
export default defineComponent({
components: { EdgeAnchor },
props: {
edge: {
type: Object as PropType<EdgeProps['edge']>,
required: true
},
markerEndId: {
type: Object as PropType<EdgeProps['markerEndId']>,
required: false,
default: undefined
},
edgeUpdaterRadius: {
type: Number as PropType<EdgeProps['edgeUpdaterRadius']>,
required: false,
default: undefined
},
type: {
type: Object,
required: true
},
onlyRenderVisibleElements: {
type: Boolean as PropType<EdgeProps['onlyRenderVisibleElements']>,
required: false,
default: false
},
connectionMode: {
type: String as PropType<EdgeProps['connectionMode']>,
required: false,
default: undefined
}
},
setup(props) {
const store = inject<RevueFlowStore>('store')!;
const hooks = inject<RevueFlowHooks>('hooks')!;
const nodes = computed(() => getSourceTargetNodes(props.edge, store.nodes));
hooks.connect.on((connection) => {
hooks.edgeUpdate.trigger({ edge: props.edge, connection });
});
if (!nodes.value.sourceNode) {
console.warn(`couldn't create edge for source id: ${props.edge.source}; edge id: ${props.edge.id}`);
}
if (!nodes.value.targetNode) {
console.warn(`couldn't create edge for target id: ${props.edge.target}; edge id: ${props.edge.id}`);
}
const targetNodeBounds = computed(() => nodes.value.targetNode?.__rf.handleBounds);
// when connection type is loose we can define all handles as sources
const targetNodeHandles = computed(() =>
props.connectionMode === ConnectionMode.Strict
? targetNodeBounds.value?.target
: targetNodeBounds.value?.target || targetNodeBounds.value?.source
);
const sourceHandle = computed(
() => nodes.value.sourceNode && getHandle(nodes.value.sourceNode.__rf.handleBounds.source, props.edge.sourceHandle || null)
);
const targetHandle = computed(() => getHandle(targetNodeHandles.value, props.edge.targetHandle || null));
const sourcePosition = computed(() => (sourceHandle.value ? sourceHandle.value.position : Position.Bottom));
const targetPosition = computed(() => (targetHandle.value ? targetHandle.value.position : Position.Top));
const edgePositions = computed<{
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
}>(() => {
return getEdgePositions(
nodes.value.sourceNode as Node,
sourceHandle.value,
sourcePosition.value,
nodes.value.targetNode as Node,
targetHandle.value,
targetPosition.value
);
});
const isVisible = computed(() => {
return props.onlyRenderVisibleElements
? edgePositions.value &&
isEdgeVisible({
sourcePos: { x: edgePositions.value?.sourceX, y: edgePositions.value?.sourceY },
targetPos: { x: edgePositions.value?.targetX, y: edgePositions.value?.targetY },
width: store.width || 0,
height: store.height || 0,
transform: store.transform
})
: true;
});
const isSelected = computed(() => store.selectedElements?.some((elm) => isEdge(elm) && elm.id === props.edge.id) || false);
const updating = ref<boolean>(false);
const inactive = computed(() => !store.elementsSelectable);
const edgeClasses = computed(() => [
'revue-flow__edge',
`revue-flow__edge-${props.type}`,
{ selected: isSelected.value, animated: props.edge.animated, inactive: inactive.value, updating: updating.value }
]);
const edgeElement = computed<Edge>(() => {
const el: Edge = {
id: props.edge.id || '',
source: props.edge.source,
target: props.edge.target,
type: props.edge.type
};
if (props.edge.sourceHandle) {
el.sourceHandle = props.edge.sourceHandle;
}
if (props.edge.targetHandle) {
el.targetHandle = props.edge.targetHandle;
}
if (typeof props.edge.data !== 'undefined') {
el.data = props.edge.data;
}
return el;
});
const onEdgeClick = (event: MouseEvent) => {
if (store.elementsSelectable) {
store.unsetNodesSelection();
store.addSelectedElements(edgeElement.value as any);
}
hooks.edgeClick.trigger({ event, edge: edgeElement.value });
};
const onEdgeContextMenu = (event: MouseEvent) =>
hooks.edgeContextMenu.trigger({
event,
edge: edgeElement.value
});
const onEdgeMouseEnter = (event: MouseEvent) => hooks.edgeMouseEnter.trigger({ event, edge: edgeElement.value });
const onEdgeMouseMove = (event: MouseEvent) => hooks.edgeMouseMove.trigger({ event, edge: edgeElement.value });
const onEdgeMouseLeave = (event: MouseEvent) => hooks.edgeMouseLeave.trigger({ event, edge: edgeElement.value });
const handleEdgeUpdater = (event: MouseEvent, isSourceHandle: boolean) => {
const nodeId = isSourceHandle ? props.edge.target : props.edge.source;
const handleId = isSourceHandle ? props.edge.targetHandle : props.edge.sourceHandle;
const isValidConnection = () => true;
const isTarget = isSourceHandle;
hooks.edgeUpdateStart.trigger({ event, edge: edgeElement.value });
onMouseDown(
event,
store,
hooks,
handleId as string,
nodeId,
isTarget,
isValidConnection,
isSourceHandle ? 'target' : 'source'
);
};
const onEdgeUpdaterSourceMouseDown = (event: MouseEvent) => {
handleEdgeUpdater(event, true);
};
const onEdgeUpdaterTargetMouseDown = (event: MouseEvent) => {
handleEdgeUpdater(event, false);
};
const onEdgeUpdaterMouseEnter = () => (updating.value = true);
const onEdgeUpdaterMouseOut = () => (updating.value = false);
return () =>
isVisible.value ? (
<g
class={edgeClasses.value}
onClick={onEdgeClick}
onContextmenu={onEdgeContextMenu}
onMouseenter={onEdgeMouseEnter}
onMousemove={onEdgeMouseMove}
onMouseleave={onEdgeMouseLeave}
>
{h(props.type, {
id: props.edge.id,
source: props.edge.source,
target: props.edge.target,
selected: isSelected,
animated: props.edge.animated,
label: props.edge.label,
labelStyle: props.edge.labelStyle,
labelShowBg: props.edge.labelShowBg,
labelBgStyle: props.edge.labelBgStyle,
labelBgPadding: props.edge.labelBgPadding,
labelBgBorderRadius: props.edge.labelBgBorderRadius,
data: props.edge.data,
style: props.edge.style,
arrowHeadType: props.edge.arrowHeadType,
sourceX: edgePositions.value.sourceX,
sourceY: edgePositions.value.sourceY,
targetX: edgePositions.value.targetX,
targetY: edgePositions.value.targetY,
sourcePosition: sourcePosition.value,
targetPosition: targetPosition.value,
markerEndId: props.markerEndId,
sourceHandleId: props.edge.sourceHandle,
targetHandleId: props.edge.targetHandle
}, {})}
<g onMousedown={onEdgeUpdaterSourceMouseDown} onMouseenter={onEdgeUpdaterMouseEnter} onMouseout={onEdgeUpdaterMouseOut}>
<EdgeAnchor
position={sourcePosition.value}
centerX={edgePositions.value.sourceX}
centerY={edgePositions.value.sourceY}
radius={props.edgeUpdaterRadius}
/>
</g>
<g onMousedown={onEdgeUpdaterTargetMouseDown} onMouseenter={onEdgeUpdaterMouseEnter} onMouseout={onEdgeUpdaterMouseOut}>
<EdgeAnchor
position={sourcePosition.value}
centerX={edgePositions.value.sourceX}
centerY={edgePositions.value.sourceY}
radius={props.edgeUpdaterRadius}
/>
</g>
</g>
) : (
''
);
}
});
-2
View File
@@ -144,8 +144,6 @@ export default defineComponent({
targetY: number; targetY: number;
}>(() => { }>(() => {
return ( return (
nodes.value.sourceNode &&
nodes.value.targetNode &&
getEdgePositions( getEdgePositions(
nodes.value.sourceNode, nodes.value.sourceNode,
sourceHandle.value, sourceHandle.value,
-70
View File
@@ -1,70 +0,0 @@
import { ElementId, Position, RevueFlowStore } from '../../types';
import { onMouseDown, ValidConnectionFunc } from './handler';
import { computed, defineComponent, h, inject, PropType } from 'vue';
import { RevueFlowHooks } from '../../hooks/RevueFlowHooks';
const alwaysValid = () => true;
export default defineComponent({
props: {
type: {
type: String,
required: false,
default: 'source'
},
position: {
type: String as PropType<Position>,
required: false,
default: Position.Top
},
isValidConnection: {
type: Function as PropType<ValidConnectionFunc>,
required: false,
default: alwaysValid
},
isConnectable: {
type: Boolean,
required: false,
default: true
},
id: {
type: String,
required: false,
default: undefined
}
},
setup(props, { slots }) {
const store = inject<RevueFlowStore>('store')!;
const hooks = inject<RevueFlowHooks>('hooks')!;
const nodeId = inject<ElementId>('NodeIdContext') as ElementId;
const isTarget = computed(() => props.type === 'target');
const onMouseDownHandler = (event: MouseEvent) => {
onMouseDown(event, store, hooks, props.id || '', nodeId, isTarget.value, props.isValidConnection);
};
const handleClasses = computed(() => [
'revue-flow__handle',
`revue-flow__handle-${props.position}`,
'nodrag',
{
source: !isTarget.value,
target: isTarget.value,
connectable: props.isConnectable
}
]);
return () =>
h(
'div',
{
dataHandleid: props.id,
dataNodeid: nodeId,
dataHandlepos: props.position,
class: handleClasses.value,
onMouseDown: onMouseDownHandler
},
slots.default?.()
);
}
});
+1 -4
View File
@@ -1,11 +1,8 @@
import { getHostForElement } from '../../utils'; import { getHostForElement } from '../../utils';
import { ElementId, XYPosition, ConnectionMode, SetConnectionId, Connection, HandleType, RevueFlowStore } from '../../types'; import { ElementId, ConnectionMode, Connection, HandleType, RevueFlowStore } from '../../types';
import { RevueFlowHooks } from '../../hooks/RevueFlowHooks'; import { RevueFlowHooks } from '../../hooks/RevueFlowHooks';
export type ValidConnectionFunc = (connection: Connection) => boolean; export type ValidConnectionFunc = (connection: Connection) => boolean;
export type SetSourceIdFunc = (params: SetConnectionId) => void;
export type SetPosition = (pos: XYPosition) => void;
type Result = { type Result = {
elementBelow: Element | null; elementBelow: Element | null;
+6 -11
View File
@@ -5,7 +5,7 @@ import { RevueFlowHooks } from '../../hooks/RevueFlowHooks';
const alwaysValid = () => true; const alwaysValid = () => true;
const Handle = defineComponent({ export default defineComponent({
props: { props: {
type: { type: {
type: String, type: String,
@@ -41,21 +41,18 @@ const Handle = defineComponent({
setup(props, { slots }) { setup(props, { slots }) {
const store = inject<RevueFlowStore>('store')!; const store = inject<RevueFlowStore>('store')!;
const hooks = inject<RevueFlowHooks>('hooks')!; const hooks = inject<RevueFlowHooks>('hooks')!;
const nodeId = inject<ElementId>('NodeIdContext') as ElementId; const nodeId = inject<ElementId>('NodeIdContext')!;
const isTarget = computed(() => props.type === 'target');
const onMouseDownHandler = (event: MouseEvent) => { const onMouseDownHandler = (event: MouseEvent) =>
console.log('mousedown'); onMouseDown(event, store, hooks, props.id as string, nodeId, props.type === 'target', props.isValidConnection);
onMouseDown(event, store, hooks, props.id as string, nodeId, isTarget.value, props.isValidConnection);
};
const handleClasses = computed(() => [ const handleClasses = computed(() => [
'revue-flow__handle', 'revue-flow__handle',
`revue-flow__handle-${props.position}`, `revue-flow__handle-${props.position}`,
'nodrag', 'nodrag',
{ {
source: !isTarget.value, source: props.type !== 'target',
target: isTarget.value, target: props.type === 'target',
connectable: props.isConnectable connectable: props.isConnectable
} }
]); ]);
@@ -73,5 +70,3 @@ const Handle = defineComponent({
); );
} }
}); });
export default Handle;
+1 -1
View File
@@ -57,7 +57,7 @@ export default defineComponent({
required: true required: true
}, },
type: { type: {
type: Object, type: [Object, String],
required: true required: true
}, },
snapToGrid: { snapToGrid: {
+17 -19
View File
@@ -38,9 +38,6 @@ export default defineComponent({
const store = inject<RevueFlowStore>('store')!; const store = inject<RevueFlowStore>('store')!;
const el = templateRef('user-selection', null); const el = templateRef('user-selection', null);
const shouldRender = computed(() => store.selectionActive || store.elementsSelectable); const shouldRender = computed(() => store.selectionActive || store.elementsSelectable);
if (!shouldRender.value) {
return null;
}
const onMouseDown = (event: MouseEvent) => { const onMouseDown = (event: MouseEvent) => {
const mousePos = getMousePosition(event); const mousePos = getMousePosition(event);
@@ -52,12 +49,10 @@ export default defineComponent({
}; };
const onMouseMove = (event: MouseEvent) => { const onMouseMove = (event: MouseEvent) => {
if (!store.selectionActive) { if (!store.selectionActive) {
return; return;
} }
const mousePos = getMousePosition(event); const mousePos = getMousePosition(event);
if (!mousePos) { if (!mousePos) {
return; return;
} }
@@ -80,19 +75,22 @@ export default defineComponent({
useEventListener(el, 'mouseup', onMouseUp); useEventListener(el, 'mouseup', onMouseUp);
useEventListener(el, 'mouseleave', onMouseLeave); useEventListener(el, 'mouseleave', onMouseLeave);
return () => ( return () =>
<div class="revue-flow__selectionpane" ref="user-selection"> shouldRender.value ? (
{store?.userSelectionRect.draw ? ( <div class="revue-flow__selectionpane" ref="user-selection">
<SelectionRect {store?.userSelectionRect.draw ? (
width={store.userSelectionRect.width} <SelectionRect
height={store.userSelectionRect.height} width={store.userSelectionRect.width}
x={store.userSelectionRect.x} height={store.userSelectionRect.height}
y={store.userSelectionRect.y} x={store.userSelectionRect.x}
/> y={store.userSelectionRect.y}
) : ( />
'' ) : (
)} ''
</div> )}
); </div>
) : (
''
);
} }
}); });
+22
View File
@@ -0,0 +1,22 @@
import { FunctionalComponent } from 'vue';
interface MarkerProps {
id: string;
}
export default ((props: MarkerProps, { slots }) => {
return () => (
<marker
id={props.id}
class="revue-flow__arrowhead"
markerWidth="12.5"
markerHeight="12.5"
viewBox="-10 -10 20 20"
orient="auto"
refX="0"
refY="0"
>
{slots.default ? slots.default() : ''}
</marker>
);
}) as FunctionalComponent<MarkerProps>;
-30
View File
@@ -1,30 +0,0 @@
<template>
<marker
:id="id"
class="revue-flow__arrowhead"
markerWidth="12.5"
markerHeight="12.5"
viewBox="-10 -10 20 20"
orient="auto"
refX="0"
refY="0"
>
<slot></slot>
</marker>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
interface MarkerProps {
id: string;
}
export default defineComponent({
props: {
id: {
type: String as PropType<MarkerProps['id']>,
required: true
}
}
});
</script>
@@ -0,0 +1,42 @@
import { defineComponent, PropType } from 'vue';
import Marker from './Marker';
interface MarkerDefinitionsProps {
color: string;
}
export default defineComponent({
components: { Marker },
props: {
color: {
type: String as PropType<MarkerDefinitionsProps['color']>,
required: true
}
},
setup(props) {
return () => (
<defs>
<Marker id="revue-flow__arrowclosed">
<polyline
stroke={props.color}
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1"
fill={props.color}
points="-5,-4 0,0 -5,4 -5,-4"
/>
</Marker>
<Marker id="revue-flow__arrow">
<polyline
stroke={props.color}
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
fill="none"
points="-5,-4 0,0 -5,4"
/>
</Marker>
</defs>
);
}
});
@@ -1,45 +0,0 @@
<template>
<defs>
<Marker id="revue-flow__arrowclosed">
<polyline
:stroke="color"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1"
:fill="color"
points="-5,-4 0,0 -5,4 -5,-4"
/>
</Marker>
<Marker id="revue-flow__arrow">
<polyline
:stroke="color"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
fill="none"
points="-5,-4 0,0 -5,4"
/>
</Marker>
</defs>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import Marker from './Marker.vue';
interface MarkerDefinitionsProps {
color: string;
}
const MarkerDefinitions = defineComponent({
name: 'MarkerDefinitions',
components: { Marker },
props: {
color: {
type: String as PropType<MarkerDefinitionsProps['color']>,
required: true
}
}
});
export default MarkerDefinitions;
</script>
@@ -1,30 +1,8 @@
<template>
<svg :width="width" :height="height" class="revue-flow__edges">
<MarkerDefinitions :color="arrowHeadColor" />
<g :transform="transformStyle">
<template v-for="edge of edges" :key="edge.id">
<Edge
:edge="edge"
:marker-end-id="markerEndId"
:onlyRenderVisibleElements="onlyRenderVisibleElements"
:type="edgeTypes[edge.type]"
/>
<ConnectionLine
v-if="renderConnectionLine"
:connectionLineStyle="connectionLineStyle"
:connectionLineType="connectionLineType"
:CustomConnectionLineComponent="connectionLineComponent"
/>
</template>
</g>
</svg>
</template>
<script lang="ts">
import { computed, CSSProperties, defineComponent, inject, PropType } from 'vue'; import { computed, CSSProperties, defineComponent, inject, PropType } from 'vue';
import Edge from '../../components/Edges/Edge.vue';
import ConnectionLine from '../../components/ConnectionLine/ConnectionLine.vue';
import { ConnectionLineType, ConnectionLineComponent, ConnectionMode, RevueFlowStore } from '../../types'; import { ConnectionLineType, ConnectionLineComponent, ConnectionMode, RevueFlowStore } from '../../types';
import MarkerDefinitions from './MarkerDefinitions.vue'; import ConnectionLine from '../../components/ConnectionLine';
import MarkerDefinitions from './MarkerDefinitions';
import Edge from '../../components/Edges/Edge';
interface EdgeRendererProps { interface EdgeRendererProps {
edgeTypes: any; edgeTypes: any;
@@ -38,7 +16,7 @@ interface EdgeRendererProps {
edgeUpdaterRadius?: number; edgeUpdaterRadius?: number;
} }
const EdgeRenderer = defineComponent({ export default defineComponent({
name: 'EdgeRenderer', name: 'EdgeRenderer',
components: { components: {
Edge, Edge,
@@ -48,8 +26,7 @@ const EdgeRenderer = defineComponent({
props: { props: {
edgeTypes: { edgeTypes: {
type: Object, type: Object,
required: false, required: true
default: undefined
}, },
connectionLineType: { connectionLineType: {
type: String as PropType<EdgeRendererProps['connectionLineType']>, type: String as PropType<EdgeRendererProps['connectionLineType']>,
@@ -92,27 +69,36 @@ const EdgeRenderer = defineComponent({
default: undefined default: undefined
} }
}, },
setup() { setup(props) {
const store = inject<RevueFlowStore>('store')!; const store = inject<RevueFlowStore>('store');
const edges = computed(() => store.edges); const transform = computed(() => store?.transform);
const transform = computed(() => store.transform);
const transformStyle = computed(() => { const transformStyle = computed(() => {
return `translate(${transform.value?.[0]},${transform.value?.[1]}) scale(${transform.value?.[2]})`; return `translate(${transform.value?.[0]},${transform.value?.[1]}) scale(${transform.value?.[2]})`;
}); });
const renderConnectionLine = computed(() => store.connectionNodeId && store.connectionHandleType); const renderConnectionLine = computed(() => store?.connectionNodeId && store?.connectionHandleType);
const width = computed(() => store.width);
const height = computed(() => store.height);
return { return () => (
transform, <svg width={store?.width} height={store?.height} class="revue-flow__edges">
transformStyle, <MarkerDefinitions color={props.arrowHeadColor as string} />
renderConnectionLine, <g transform={transformStyle.value}>
edges, {store?.edges.map((edge) => (
width, <Edge
height key={edge.id}
}; edge={edge}
type={props.edgeTypes[edge.type || 'default']}
onlyRenderVisibleElements={props.onlyRenderVisibleElements}
markerEndId={props.markerEndId}
/>
))}
{renderConnectionLine.value && (
<ConnectionLine
connectionLineStyle={props.connectionLineStyle}
connectionLineType={props.connectionLineType}
customConnectionLine={props.connectionLineComponent}
/>
)}
</g>
</svg>
);
} }
}); });
export default EdgeRenderer;
</script>
@@ -1,26 +1,3 @@
<template>
<ZoomPane
:selectionKeyPressed="keyPressed"
:elementsSelectable="elementsSelectable"
:zoomOnScroll="zoomOnScroll"
:zoomOnPinch="zoomOnPinch"
:panOnScroll="panOnScroll"
:panOnScrollSpeed="panOnScrollSpeed"
:panOnScrollMode="panOnScrollMode"
:zoomOnDoubleClick="zoomOnDoubleClick"
:paneMoveable="paneMoveable"
:defaultPosition="defaultPosition"
:defaultZoom="defaultZoom"
:translateExtent="translateExtent"
:zoomActivationKeyCode="zoomActivationKeyCode"
>
<slot></slot>
<UserSelection v-if="keyPressed" />
<NodesSelection v-if="selectionActive" />
<div class="revue-flow__pane" @click="onClick" @onContextmenu="onContextMenu" @wheel="onWheel" />
</ZoomPane>
</template>
<script lang="ts">
import { computed, defineComponent, inject, PropType } from 'vue'; import { computed, defineComponent, inject, PropType } from 'vue';
import ZoomPane from '../ZoomPane'; import ZoomPane from '../ZoomPane';
import UserSelection from '../../components/UserSelection'; import UserSelection from '../../components/UserSelection';
@@ -43,7 +20,7 @@ type FlowRendererProps = Omit<
| 'selectNodesOnDrag' | 'selectNodesOnDrag'
>; >;
const FlowRenderer = defineComponent({ export default defineComponent({
name: 'FlowRenderer', name: 'FlowRenderer',
components: { UserSelection, ZoomPane, NodesSelection }, components: { UserSelection, ZoomPane, NodesSelection },
props: { props: {
@@ -123,7 +100,7 @@ const FlowRenderer = defineComponent({
default: undefined default: undefined
} }
}, },
setup(props) { setup(props, { slots }) {
const store = inject<RevueFlowStore>('store')!; const store = inject<RevueFlowStore>('store')!;
const hooks = inject<RevueFlowHooks>('hooks')!; const hooks = inject<RevueFlowHooks>('hooks')!;
const keyPressed = useKeyPress(props.selectionKeyCode); const keyPressed = useKeyPress(props.selectionKeyCode);
@@ -147,15 +124,27 @@ const FlowRenderer = defineComponent({
const selectionActive = computed(() => store.nodesSelectionActive); const selectionActive = computed(() => store.nodesSelectionActive);
return { return () => (
onClick, <ZoomPane
onContextMenu, selectionKeyPressed={keyPressed.value}
onWheel, elementsSelectable={props.elementsSelectable}
keyPressed, zoomOnScroll={props.zoomOnScroll}
selectionActive zoomOnPinch={props.zoomOnPinch}
}; panOnScroll={props.panOnScroll}
panOnScrollSpeed={props.panOnScrollSpeed}
panOnScrollMode={props.panOnScrollMode}
zoomOnDoubleClick={props.zoomOnDoubleClick}
paneMoveable={props.paneMoveable}
defaultPosition={props.defaultPosition}
defaultZoom={props.defaultZoom}
translateExtent={props.translateExtent}
zoomActivationKeyCode={props.zoomActivationKeyCode}
>
{slots.default ? slots.default() : ''}
{keyPressed.value ? <UserSelection /> : ''}
{selectionActive.value ? <NodesSelection /> : ''}
<div class="revue-flow__pane" onClick={onClick} onContextmenu={onContextMenu} onWheel={onWheel} />
</ZoomPane>
);
} }
}); });
export default FlowRenderer;
</script>
+45 -49
View File
@@ -1,52 +1,52 @@
<template> <template>
<FlowRenderer <FlowRenderer
:deleteKeyCode="deleteKeyCode" :delete-key-code="deleteKeyCode"
:selectionKeyCode="selectionKeyCode" :selection-key-code="selectionKeyCode"
:multiSelectionKeyCode="multiSelectionKeyCode" :multi-selection-key-code="multiSelectionKeyCode"
:zoomActivationKeyCode="zoomActivationKeyCode" :zoom-activation-key-code="zoomActivationKeyCode"
:elementsSelectable="elementsSelectable" :elements-selectable="elementsSelectable"
:zoomOnScroll="zoomOnScroll" :zoom-on-scroll="zoomOnScroll"
:zoomOnPinch="zoomOnPinch" :zoom-on-pinch="zoomOnPinch"
:zoomOnDoubleClick="zoomOnDoubleClick" :zoom-on-double-click="zoomOnDoubleClick"
:panOnScroll="panOnScroll" :pan-on-scroll="panOnScroll"
:panOnScrollSpeed="panOnScrollSpeed" :pan-on-scroll-speed="panOnScrollSpeed"
:panOnScrollMode="panOnScrollMode" :pan-on-scroll-mode="panOnScrollMode"
:paneMoveable="paneMoveable" :pane-moveable="paneMoveable"
:defaultPosition="defaultPosition" :default-position="defaultPosition"
:defaultZoom="defaultZoom" :default-zoom="defaultZoom"
:translateExtent="translateExtent" :translate-extent="translateExtent"
> >
<NodeRenderer <NodeRenderer
:nodeTypes="nodeTypes" :node-types="nodeTypes"
:selectNodesOnDrag="selectNodesOnDrag" :select-nodes-on-drag="selectNodesOnDrag"
:snapToGrid="snapToGrid" :snap-to-grid="snapToGrid"
:snapGrid="snapGrid" :snap-grid="snapGrid"
:onlyRenderVisibleElements="onlyRenderVisibleElements" :only-render-visible-elements="onlyRenderVisibleElements"
/> />
<EdgeRenderer <EdgeRenderer
:edgeTypes="edgeTypes" :edge-types="edgeTypes"
:connectionLineType="connectionLineType" :connection-line-type="connectionLineType"
:connectionLineStyle="connectionLineStyle" :connection-line-style="connectionLineStyle"
:connectionLineComponent="connectionLineComponent" :connection-line-component="connectionLineComponent"
:connectionMode="connectionMode" :connection-mode="connectionMode"
:arrowHeadColor="arrowHeadColor" :arrow-head-color="arrowHeadColor"
:markerEndId="markerEndId" :marker-end-id="markerEndId"
:onlyRenderVisibleElements="onlyRenderVisibleElements" :only-render-visible-elements="onlyRenderVisibleElements"
:edgeUpdaterRadius="edgeUpdaterRadius" :edge-updater-radius="edgeUpdaterRadius"
/> />
</FlowRenderer> </FlowRenderer>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, inject, PropType, CSSProperties, watchEffect } from 'vue'; import { defineComponent, inject, PropType, CSSProperties, watchEffect } from 'vue';
import FlowRenderer from '../FlowRenderer/FlowRenderer.vue'; import FlowRenderer from '../FlowRenderer';
import NodeRenderer from '../NodeRenderer/NodeRenderer.vue'; import NodeRenderer from '../NodeRenderer';
import EdgeRenderer from '../EdgeRenderer/EdgeRenderer.vue'; import EdgeRenderer from '../EdgeRenderer';
import { onLoadProject, onLoadGetElements, onLoadToObject } from '../../utils/graph'; import { onLoadProject, onLoadGetElements, onLoadToObject } from '../../utils/graph';
import { GraphViewProps, RevueFlowStore } from '../../types'; import { GraphViewProps, RevueFlowStore } from '../../types';
import useZoomPanHelper from '../../hooks/useZoomPanHelper'; import useZoomPanHelper from '../../hooks/useZoomPanHelper';
import { RevueFlowHooks } from '../../hooks/RevueFlowHooks'; import { RevueFlowHooks } from '../../hooks/RevueFlowHooks';
const GraphView = defineComponent({ export default defineComponent({
name: 'GraphView', name: 'GraphView',
components: { NodeRenderer, EdgeRenderer, FlowRenderer }, components: { NodeRenderer, EdgeRenderer, FlowRenderer },
props: { props: {
@@ -220,20 +220,18 @@ const GraphView = defineComponent({
const store = inject<RevueFlowStore>('store')!; const store = inject<RevueFlowStore>('store')!;
const hooks = inject<RevueFlowHooks>('hooks')!; const hooks = inject<RevueFlowHooks>('hooks')!;
const { onReady } = useZoomPanHelper(store); const { onReady } = useZoomPanHelper();
onReady(({ zoomIn, zoomOut, zoomTo, transform, fitView, initialized }) => { onReady(({ zoomIn, zoomOut, zoomTo, transform, fitView }) => {
if (initialized) { hooks.load.trigger({
hooks.load.trigger({ fitView: (params = { padding: 0.1 }) => fitView(params),
fitView: (params = { padding: 0.1 }) => fitView(params), zoomIn,
zoomIn, zoomOut,
zoomOut, zoomTo,
zoomTo, setTransform: transform,
setTransform: transform, project: onLoadProject(store),
project: onLoadProject(store), getElements: onLoadGetElements(store),
getElements: onLoadGetElements(store), toObject: onLoadToObject(store)
toObject: onLoadToObject(store) });
});
}
}); });
const init = () => { const init = () => {
@@ -281,6 +279,4 @@ const GraphView = defineComponent({
}); });
} }
}); });
export default GraphView;
</script> </script>
@@ -1,21 +1,7 @@
<template>
<div class="revue-flow__nodes" :style="transformStyle">
<template v-for="(node, i) of visibleNodes" :key="`node-${i}`">
<Node
:node="node"
:snap-grid="snapGrid"
:snap-to-grid="snapToGrid"
:select-nodes-on-drag="selectNodesOnDrag"
:type="nodeTypes[node.type]"
/>
</template>
</div>
</template>
<script lang="ts">
import { getNodesInside } from '../../utils/graph';
import { NodeTypesType, RevueFlowStore } from '../../types';
import { computed, defineComponent, inject, PropType } from 'vue'; import { computed, defineComponent, inject, PropType } from 'vue';
import Node from '../../components/Nodes/Node.vue'; import Node from '../../components/Nodes/Node.vue';
import { getNodesInside } from '../../utils/graph';
import { NodeTypesType, RevueFlowStore } from '../../types';
interface NodeRendererProps { interface NodeRendererProps {
nodeTypes: NodeTypesType; nodeTypes: NodeTypesType;
@@ -25,7 +11,7 @@ interface NodeRendererProps {
onlyRenderVisibleElements: boolean; onlyRenderVisibleElements: boolean;
} }
const NodeRenderer = defineComponent({ export default defineComponent({
name: 'NodeRenderer', name: 'NodeRenderer',
components: { Node }, components: { Node },
props: { props: {
@@ -79,12 +65,28 @@ const NodeRenderer = defineComponent({
transform: `translate(${store.transform[0]}px,${store.transform[1]}px) scale(${store.transform[2]})` transform: `translate(${store.transform[0]}px,${store.transform[1]}px) scale(${store.transform[2]})`
})); }));
return { return () => (
transformStyle, <div class="revue-flow__nodes" style={transformStyle.value}>
visibleNodes {visibleNodes.value?.map((node) => {
}; const nodeType = node.type || 'default';
if (props.nodeTypes) {
const type = props.nodeTypes[nodeType] || props.nodeTypes.default;
if (!props.nodeTypes[nodeType]) {
console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`);
}
return (
<Node
node={node}
snapGrid={props.snapGrid}
snapToGrid={props.snapToGrid}
selectNodesOnDrag={props.selectNodesOnDrag}
type={type}
/>
);
}
})}
</div>
);
} }
}); });
export default NodeRenderer;
</script>
+37 -39
View File
@@ -1,39 +1,39 @@
<template> <template>
<div class="revue-flow"> <div class="revue-flow">
<GraphView <GraphView
:nodeTypes="nodeTypesParsed" :node-types="nodeTypesParsed"
:edgeTypes="edgeTypesParsed" :edge-types="edgeTypesParsed"
:connectionMode="connectionMode" :connection-mode="connectionMode"
:connectionLineType="connectionLineType" :connection-line-type="connectionLineType"
:connectionLineStyle="connectionLineStyle" :connection-line-style="connectionLineStyle"
:connectionLineComponent="connectionLineComponent" :connection-line-component="connectionLineComponent"
:selectionKeyCode="selectionKeyCode" :selection-key-code="selectionKeyCode"
:deleteKeyCode="deleteKeyCode" :delete-key-code="deleteKeyCode"
:multiSelectionKeyCode="multiSelectionKeyCode" :multi-selection-key-code="multiSelectionKeyCode"
:zoomActivationKeyCode="zoomActivationKeyCode" :zoom-activation-key-code="zoomActivationKeyCode"
:snapToGrid="snapToGrid" :snap-to-grid="snapToGrid"
:snapGrid="snapGrid" :snap-grid="snapGrid"
:onlyRenderVisibleElements="onlyRenderVisibleElements" :only-render-visible-elements="onlyRenderVisibleElements"
:nodesDraggable="nodesDraggable" :nodes-draggable="nodesDraggable"
:nodesConnectable="nodesConnectable" :nodes-connectable="nodesConnectable"
:elementsSelectable="elementsSelectable" :elements-selectable="elementsSelectable"
:selectNodesOnDrag="selectNodesOnDrag" :select-nodes-on-drag="selectNodesOnDrag"
:minZoom="minZoom" :min-zoom="minZoom"
:maxZoom="maxZoom" :max-zoom="maxZoom"
:defaultZoom="defaultZoom" :default-zoom="defaultZoom"
:defaultPosition="defaultPosition" :default-position="defaultPosition"
:translateExtent="translateExtent" :translate-extent="translateExtent"
:nodeExtent="nodeExtent" :node-extent="nodeExtent"
:arrowHeadColor="arrowHeadColor" :arrow-head-color="arrowHeadColor"
:markerEndId="markerEndId" :marker-end-id="markerEndId"
:zoomOnScroll="zoomOnScroll" :zoom-on-scroll="zoomOnScroll"
:zoomOnPinch="zoomOnPinch" :zoom-on-pinch="zoomOnPinch"
:zoomOnDoubleClick="zoomOnDoubleClick" :zoom-on-double-click="zoomOnDoubleClick"
:panOnScroll="panOnScroll" :pan-on-scroll="panOnScroll"
:panOnScrollSpeed="panOnScrollSpeed" :pan-on-scroll-speed="panOnScrollSpeed"
:panOnScrollMode="panOnScrollMode" :pan-on-scroll-mode="panOnScrollMode"
:paneMoveable="paneMoveable" :pane-moveable="paneMoveable"
:edgeUpdaterRadius="edgeUpdaterRadius" :edge-updater-radius="edgeUpdaterRadius"
/> />
<slot></slot> <slot></slot>
</div> </div>
@@ -68,8 +68,7 @@ const defaultEdgeTypes = {
smoothstep: SmoothStepEdge smoothstep: SmoothStepEdge
}; };
const eventHooks = useRevueFlow(); export default defineComponent({
const RevueFlow = defineComponent({
name: 'RevueFlow', name: 'RevueFlow',
components: { GraphView }, components: { GraphView },
props: { props: {
@@ -254,12 +253,12 @@ const RevueFlow = defineComponent({
default: 10 default: 10
} }
}, },
emits: Object.keys(eventHooks), emits: Object.keys(useRevueFlow()),
setup(props, { emit }) { setup(props, { emit }) {
const store = configureStore(initialState)(); const store = configureStore(initialState)();
provide<RevueFlowStore>('store', store); provide<RevueFlowStore>('store', store);
eventHooks.bind(emit); const hooks = useRevueFlow().bind(emit);
provide<RevueFlowHooks>('hooks', eventHooks); provide<RevueFlowHooks>('hooks', hooks);
const elements = useVModel(props, 'modelValue', emit); const elements = useVModel(props, 'modelValue', emit);
store.setElements(elements.value); store.setElements(elements.value);
@@ -289,5 +288,4 @@ const RevueFlow = defineComponent({
}; };
} }
}); });
export default RevueFlow;
</script> </script>
+70 -104
View File
@@ -1,11 +1,12 @@
import { zoom, zoomIdentity } from 'd3-zoom'; import { D3ZoomEvent, zoom, zoomIdentity, ZoomTransform } from 'd3-zoom';
import { select, pointer } from 'd3-selection'; import { select, pointer } from 'd3-selection';
import { clamp } from '../../utils'; import { clamp } from '../../utils';
import { FlowTransform, TranslateExtent, PanOnScrollMode, KeyCode, RevueFlowStore } from '../../types'; import { FlowTransform, TranslateExtent, PanOnScrollMode, KeyCode, RevueFlowStore } from '../../types';
import { defineComponent, inject, PropType, ref, watchEffect } from 'vue'; import { defineComponent, inject, PropType, ref, watchEffect } from 'vue';
import useKeyPress from '../../hooks/useKeyPress'; import useKeyPress from '../../hooks/useKeyPress';
import useResizeHandler from '../../hooks/useResizeHandler'; import useResizeHandler from '../../hooks/useResizeHandler';
import { RevueFlowHooks } from '../../hooks/RevueFlowHooks';
import { templateRef, whenever } from '@vueuse/core';
interface ZoomPaneProps { interface ZoomPaneProps {
selectionKeyPressed: boolean; selectionKeyPressed: boolean;
@@ -20,38 +21,12 @@ interface ZoomPaneProps {
defaultPosition?: [number, number]; defaultPosition?: [number, number];
defaultZoom?: number; defaultZoom?: number;
translateExtent?: TranslateExtent; translateExtent?: TranslateExtent;
onMove?: (flowTransform?: FlowTransform) => void;
onMoveStart?: (flowTransform?: FlowTransform) => void;
onMoveEnd?: (flowTransform?: FlowTransform) => void;
zoomActivationKeyCode?: KeyCode; zoomActivationKeyCode?: KeyCode;
} }
const viewChanged = (prevTransform: FlowTransform, eventTransform: any): boolean => export default defineComponent({
prevTransform.x !== eventTransform.x || prevTransform.y !== eventTransform.y || prevTransform.zoom !== eventTransform.k; name: 'ZoomPane',
const eventToFlowTransform = (eventTransform: any): FlowTransform => ({
x: eventTransform.x,
y: eventTransform.y,
zoom: eventTransform.k
});
const ZoomPane = defineComponent({
props: { props: {
onMove: {
type: Function as unknown as PropType<ZoomPaneProps['onMove']>,
required: false,
default: () => {}
},
onMoveStart: {
type: Function as unknown as PropType<ZoomPaneProps['onMoveStart']>,
required: false,
default: () => {}
},
onMoveEnd: {
type: Function as unknown as PropType<ZoomPaneProps['onMoveEnd']>,
required: false,
default: () => {}
},
zoomOnScroll: { zoomOnScroll: {
type: Boolean as PropType<ZoomPaneProps['zoomOnScroll']>, type: Boolean as PropType<ZoomPaneProps['zoomOnScroll']>,
required: false, required: false,
@@ -85,12 +60,12 @@ const ZoomPane = defineComponent({
selectionKeyPressed: { selectionKeyPressed: {
type: Boolean as PropType<ZoomPaneProps['selectionKeyPressed']>, type: Boolean as PropType<ZoomPaneProps['selectionKeyPressed']>,
required: false, required: false,
default: undefined default: false
}, },
elementsSelectable: { elementsSelectable: {
type: Boolean as PropType<ZoomPaneProps['elementsSelectable']>, type: Boolean as PropType<ZoomPaneProps['elementsSelectable']>,
required: false, required: false,
default: undefined default: true
}, },
paneMoveable: { paneMoveable: {
type: Boolean as PropType<ZoomPaneProps['paneMoveable']>, type: Boolean as PropType<ZoomPaneProps['paneMoveable']>,
@@ -120,16 +95,23 @@ const ZoomPane = defineComponent({
}, },
setup(props, { slots }) { setup(props, { slots }) {
const store = inject<RevueFlowStore>('store')!; const store = inject<RevueFlowStore>('store')!;
const zoomPane = ref<HTMLDivElement | null>(null); const hooks = inject<RevueFlowHooks>('hooks')!;
const zoomPane = templateRef<HTMLDivElement>('zoom-pane', null);
const prevTransform = ref<FlowTransform>({ x: 0, y: 0, zoom: 0 }); const prevTransform = ref<FlowTransform>({ x: 0, y: 0, zoom: 0 });
const zoomActivationKeyPressed = useKeyPress(props.zoomActivationKeyCode); const zoomActivationKeyPressed = useKeyPress(props.zoomActivationKeyCode);
watchEffect(() => { const viewChanged = (prevTransform: FlowTransform, eventTransform: ZoomTransform): boolean =>
if (zoomPane.value) useResizeHandler(zoomPane.value, store.updateSize); prevTransform.x !== eventTransform.x || prevTransform.y !== eventTransform.y || prevTransform.zoom !== eventTransform.k;
const eventToFlowTransform = (eventTransform: ZoomTransform): FlowTransform => ({
x: eventTransform.x,
y: eventTransform.y,
zoom: eventTransform.k
}); });
watchEffect(() => { const { onReady } = useResizeHandler(zoomPane, store.updateSize);
if (zoomPane.value && props.defaultPosition && props.defaultZoom) { onReady(() => {
if (props.defaultPosition && props.defaultZoom) {
const currentTranslateExtent = const currentTranslateExtent =
typeof props.translateExtent !== 'undefined' ? props.translateExtent : store.translateExtent; typeof props.translateExtent !== 'undefined' ? props.translateExtent : store.translateExtent;
const d3ZoomInstance = zoom().scaleExtent([store.minZoom, store.maxZoom]).translateExtent(currentTranslateExtent); const d3ZoomInstance = zoom().scaleExtent([store.minZoom, store.maxZoom]).translateExtent(currentTranslateExtent);
@@ -152,43 +134,41 @@ const ZoomPane = defineComponent({
} }
}); });
watchEffect(() => { whenever(zoomActivationKeyPressed, () => {
if (store.d3Selection && store.d3Zoom) { if (props.panOnScroll) {
if (props.panOnScroll && zoomActivationKeyPressed) { store.d3Selection
store.d3Selection ?.on('wheel', (event: WheelEvent) => {
.on('wheel', (event: any) => { event.preventDefault();
event.preventDefault(); event.stopImmediatePropagation();
event.stopImmediatePropagation();
const currentZoom = store.d3Selection?.property('__zoom').k || 1; const currentZoom = store.d3Selection?.property('__zoom').k || 1;
if (event.ctrlKey && props.zoomOnPinch) { if (event.ctrlKey && props.zoomOnPinch) {
const point = pointer(event); const point = pointer(event);
// taken from https://github.com/d3/d3-zoom/blob/master/src/zoom.js // taken from https://github.com/d3/d3-zoom/blob/master/src/zoom.js
const pinchDelta = -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * 10; const pinchDelta = -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * 10;
const zoom = currentZoom * Math.pow(2, pinchDelta); const zoom = currentZoom * Math.pow(2, pinchDelta);
if (store.d3Selection) store.d3Zoom?.scaleTo(store.d3Selection, zoom, point); if (store.d3Selection) store.d3Zoom?.scaleTo(store.d3Selection, zoom, point);
return; return;
} }
// increase scroll speed in firefox // increase scroll speed in firefox
// firefox: deltaMode === 1; chrome: deltaMode === 0 // firefox: deltaMode === 1; chrome: deltaMode === 0
const deltaNormalize = event.deltaMode === 1 ? 20 : 1; const deltaNormalize = event.deltaMode === 1 ? 20 : 1;
const deltaX = props.panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize; const deltaX = props.panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize;
const deltaY = props.panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize; const deltaY = props.panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize;
if (store.d3Selection && props.panOnScrollSpeed) if (store.d3Selection && props.panOnScrollSpeed)
store.d3Zoom?.translateBy( store.d3Zoom?.translateBy(
store.d3Selection, store.d3Selection,
-(deltaX / currentZoom) * props.panOnScrollSpeed, -(deltaX / currentZoom) * props.panOnScrollSpeed,
-(deltaY / currentZoom) * props.panOnScrollSpeed -(deltaY / currentZoom) * props.panOnScrollSpeed
); );
}) })
.on('wheel.zoom', null); .on('wheel.zoom', null);
} else if (typeof store.d3ZoomHandler !== 'undefined') { } else if (typeof store.d3ZoomHandler !== 'undefined') {
store.d3Selection.on('wheel', null).on('wheel.zoom', store.d3ZoomHandler); store.d3Selection?.on('wheel', null).on('wheel.zoom', store.d3ZoomHandler);
}
} }
}); });
@@ -197,13 +177,9 @@ const ZoomPane = defineComponent({
if (props.selectionKeyPressed) { if (props.selectionKeyPressed) {
store.d3Zoom.on('zoom', null); store.d3Zoom.on('zoom', null);
} else { } else {
store.d3Zoom.on('zoom', (event: any) => { store.d3Zoom.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
store.transform = [event.transform.x, event.transform.y, event.transform.k]; store.transform = [event.transform.x, event.transform.y, event.transform.k];
hooks.move.trigger(eventToFlowTransform(event.transform));
if (props.onMove) {
const flowTransform = eventToFlowTransform(event.transform);
props.onMove(flowTransform);
}
}); });
} }
} }
@@ -211,41 +187,33 @@ const ZoomPane = defineComponent({
watchEffect(() => { watchEffect(() => {
if (store.d3Zoom) { if (store.d3Zoom) {
if (props.onMoveStart) { store.d3Zoom.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
store.d3Zoom.on('start', (event: any) => { if (viewChanged(prevTransform.value, event.transform)) {
if (viewChanged(prevTransform.value, event.transform)) { const flowTransform = eventToFlowTransform(event.transform);
const flowTransform = eventToFlowTransform(event.transform); prevTransform.value = flowTransform;
prevTransform.value = flowTransform;
if (props.onMoveStart) props.onMoveStart(flowTransform); hooks.moveStart.trigger(flowTransform);
} }
}); });
} else {
store.d3Zoom.on('start', null);
}
} }
}); });
watchEffect(() => { watchEffect(() => {
if (store.d3Zoom) { if (store.d3Zoom) {
if (props.onMoveEnd) { store.d3Zoom.on('end', (event: D3ZoomEvent<HTMLDivElement, any>) => {
store.d3Zoom.on('end', (event: any) => { if (viewChanged(prevTransform.value, event.transform)) {
if (viewChanged(prevTransform.value, event.transform)) { const flowTransform = eventToFlowTransform(event.transform);
const flowTransform = eventToFlowTransform(event.transform); prevTransform.value = flowTransform;
prevTransform.value = flowTransform;
if (props.onMoveEnd) props.onMoveEnd(flowTransform); hooks.moveEnd.trigger(flowTransform);
} }
}); });
} else {
store.d3Zoom.on('end', null);
}
} }
}); });
watchEffect(() => { watchEffect(() => {
if (store.d3Zoom) { if (store.d3Zoom) {
store.d3Zoom.filter((event: any) => { store.d3Zoom.filter((event: MouseEvent) => {
const zoomScroll = props.zoomOnScroll; const zoomScroll = props.zoomOnScroll;
const pinchZoom = props.zoomOnPinch && event.ctrlKey; const pinchZoom = props.zoomOnPinch && event.ctrlKey;
@@ -264,20 +232,20 @@ const ZoomPane = defineComponent({
return false; return false;
} }
if (event.target.closest('.nowheel') && event.type === 'wheel') { if ((event.target as Element).closest('.nowheel') && event.type === 'wheel') {
return false; return false;
} }
// when the target element is a node, we still allow zooming // when the target element is a node, we still allow zooming
if ( if (
(event.target.closest('.revue-flow__node') || event.target.closest('.revue-flow__edge')) && ((event.target as Element).closest('.revue-flow__node') || (event.target as Element).closest('.revue-flow__edge')) &&
event.type !== 'wheel' event.type !== 'wheel'
) { ) {
return false; return false;
} }
// when the target element is a node selection, we still allow zooming // when the target element is a node selection, we still allow zooming
if (event.target.closest('.revue-flow__nodesselection') && event.type !== 'wheel') { if ((event.target as Element).closest('.revue-flow__nodesselection') && event.type !== 'wheel') {
return false; return false;
} }
@@ -302,11 +270,9 @@ const ZoomPane = defineComponent({
}); });
return () => ( return () => (
<div class="revue-flow__renderer revue-flow__zoompane" ref={zoomPane}> <div class="revue-flow__renderer revue-flow__zoompane" ref="zoom-pane">
{slots.default ? slots.default() : ''} {slots.default ? slots.default() : ''}
</div> </div>
); );
} }
}); });
export default ZoomPane;
+3 -1
View File
@@ -44,7 +44,7 @@ export interface RevueFlowHooks {
edgeUpdateEnd: EventHook<{ event: MouseEvent; edge: Edge }>; edgeUpdateEnd: EventHook<{ event: MouseEvent; edge: Edge }>;
} }
export const useRevueFlow = (): RevueFlowHooks & { bind: (emit: (event: string, ...args: any[]) => void) => void } => { export const useRevueFlow = (): RevueFlowHooks & { bind: (emit: (event: string, ...args: any[]) => void) => RevueFlowHooks } => {
const eventHooks: RevueFlowHooks = { const eventHooks: RevueFlowHooks = {
elementClick: createEventHook<{ event: MouseEvent; element: Node | Edge }>(), elementClick: createEventHook<{ event: MouseEvent; element: Node | Edge }>(),
elementsRemove: createEventHook<Elements>(), elementsRemove: createEventHook<Elements>(),
@@ -93,6 +93,8 @@ export const useRevueFlow = (): RevueFlowHooks & { bind: (emit: (event: string,
emit(key, data); emit(key, data);
}); });
} }
return eventHooks;
}; };
return { return {
+8 -2
View File
@@ -1,10 +1,11 @@
import { createEventHook, MaybeRef, unrefElement, until, useEventListener, useResizeObserver } from '@vueuse/core';
import { getDimensions } from '../utils'; import { getDimensions } from '../utils';
import { Dimensions } from '../types'; import { Dimensions } from '../types';
import { MaybeRef, unrefElement, until, useEventListener, useResizeObserver } from '@vueuse/core';
export default (rendererNode: MaybeRef<HTMLDivElement>, updateSize: (size: Dimensions) => any) => { export default (rendererNode: MaybeRef<HTMLDivElement>, updateSize: (size: Dimensions) => any) => {
const resizeHandlerHook = createEventHook();
const updateDimensions = () => { const updateDimensions = () => {
if (!rendererNode) { if (!unrefElement(rendererNode)) {
return; return;
} }
@@ -23,5 +24,10 @@ export default (rendererNode: MaybeRef<HTMLDivElement>, updateSize: (size: Dimen
updateDimensions(); updateDimensions();
useEventListener(window, 'resize', updateDimensions); useEventListener(window, 'resize', updateDimensions);
useResizeObserver(unrefElement(rendererNode), () => updateDimensions()); useResizeObserver(unrefElement(rendererNode), () => updateDimensions());
resizeHandlerHook.trigger(true);
}); });
return {
onReady: resizeHandlerHook.on
};
}; };
+1 -2
View File
@@ -65,8 +65,7 @@ const useZoomPanHelper = (): { onReady: EventHookOn<ZoomPanHelperFunctions> } =>
project: (position: XYPosition) => { project: (position: XYPosition) => {
const { transform, snapToGrid, snapGrid } = store; const { transform, snapToGrid, snapGrid } = store;
return pointToRendererPoint(position, transform, snapToGrid, snapGrid); return pointToRendererPoint(position, transform, snapToGrid, snapGrid);
}, }
initialized: true
}); });
until(d3Zoom) until(d3Zoom)
+15
View File
@@ -0,0 +1,15 @@
import Vue, { VNode } from 'vue';
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// @ts-ignore
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any;
}
}
}
-1
View File
@@ -666,7 +666,6 @@ export interface ZoomPanHelperFunctions {
setCenter: (x: number, y: number, zoom?: number) => void; setCenter: (x: number, y: number, zoom?: number) => void;
fitBounds: (bounds: Rect, padding?: number) => void; fitBounds: (bounds: Rect, padding?: number) => void;
project: (position: XYPosition) => XYPosition; project: (position: XYPosition) => XYPosition;
initialized: boolean;
} }
export type OnEdgeUpdateFunc<T = any> = (oldEdge: Edge<T>, newConnection: Connection) => void; export type OnEdgeUpdateFunc<T = any> = (oldEdge: Edge<T>, newConnection: Connection) => void;
+4 -4
View File
@@ -262,10 +262,10 @@
"@babel/helper-validator-identifier" "^7.14.5" "@babel/helper-validator-identifier" "^7.14.5"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@braks/revue-draggable@^0.2.4": "@braks/revue-draggable@^0.2.5":
version "0.2.4" version "0.2.5"
resolved "https://registry.yarnpkg.com/@braks/revue-draggable/-/revue-draggable-0.2.4.tgz#a3af0c27f32a1845f4182ae579ce80cc88fd9019" resolved "https://registry.yarnpkg.com/@braks/revue-draggable/-/revue-draggable-0.2.5.tgz#28a94ee4633aab25bcee28877511e0c9ad262f59"
integrity sha512-8hDMuO7fl5KCshmg87aK3bQt371f3jPOZ35ip4gdcalP2fEnusVv8+AjWP1DHR2lObDTjovn30LzCK4Ily08LA== integrity sha512-iKGLFXaurfEqoyPbkANv8ZuO1229krVvUQDDYN9le5WlVssFWcQtA3hNvBD2TPuwil226qXv9SqXGp5kgDhAsA==
dependencies: dependencies:
"@vueuse/core" "^5.1.3" "@vueuse/core" "^5.1.3"
vue-demi latest vue-demi latest