Initial Commit

This commit is contained in:
Braks
2021-07-01 19:54:02 +02:00
commit 4025ec82cb
30 changed files with 4392 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<revue-flow></revue-flow>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import Index from './container/RevueFlow';
export default defineComponent({
name: 'App',
components: {
RevueFlow: Index
}
});
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

+64
View File
@@ -0,0 +1,64 @@
<template>
<h1>{{ msg }}</h1>
<p>
Recommended IDE setup:
<a href="https://code.visualstudio.com/" target="_blank">VSCode</a>
+
<a href="https://marketplace.visualstudio.com/items?itemName=octref.vetur" target="_blank"> Vetur </a>
or
<a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
(if using
<code>&lt;script setup&gt;</code>)
</p>
<p>See <code>README.md</code> for more information.</p>
<p>
<a href="https://vitejs.dev/guide/features.html" target="_blank"> Vite Docs </a>
|
<a href="https://v3.vuejs.org/" target="_blank">Vue 3 Docs</a>
</p>
<button type="button" @click="count++">count is: {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test hot module replacement.
</p>
</template>
<script lang="ts">
import { ref, defineComponent } from 'vue';
export default defineComponent({
name: 'HelloWorld',
props: {
msg: {
type: String,
required: true
}
},
setup: () => {
const count = ref(0);
return { count };
}
});
</script>
<style scoped>
a {
color: #42b983;
}
label {
margin: 0 0.5em;
font-weight: bold;
}
code {
background-color: #eee;
padding: 2px 4px;
border-radius: 4px;
color: #304455;
}
</style>
+29
View File
@@ -0,0 +1,29 @@
import '../../style.css';
import '../../theme-default.css';
import { HTMLAttributes, defineComponent } from 'vue';
const defaultNodeTypes = {
input: 'InputNode',
default: 'DefaultNode',
output: 'OutputNode'
};
const defaultEdgeTypes = {
default: 'BezierEdge',
straight: 'StraightEdge',
step: 'StepEdge',
smoothstep: 'SmoothStepEdge'
};
export interface ReactFlowProps extends Omit<HTMLAttributes, 'onLoad'> {}
export type ReactFlowRefType = HTMLDivElement;
const RevueFlow = defineComponent({
name: 'RevueFlow',
setup() {
return () => <div>Revue Flow!</div>;
}
});
export default RevueFlow;
+7
View File
@@ -0,0 +1,7 @@
import { createApp } from 'vue';
import App from './App.vue';
import { createPinia } from 'pinia';
const app = createApp(App);
app.use(createPinia());
app.mount('#app');
+5
View File
@@ -0,0 +1,5 @@
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
+187
View File
@@ -0,0 +1,187 @@
.react-flow {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.react-flow__renderer,
.react-flow__pane,
.react-flow__selectionpane {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
.react-flow__pane {
z-index: 1;
}
.react-flow__renderer {
z-index: 4;
}
.react-flow__selectionpane {
z-index: 5;
}
.react-flow__selection {
position: absolute;
top: 0;
left: 0;
}
.react-flow__edges {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
z-index: 2;
}
.react-flow__edge {
pointer-events: visibleStroke;
&.inactive {
pointer-events: none;
}
}
@keyframes dashdraw {
from {
stroke-dashoffset: 10;
}
}
.react-flow__edge-path {
fill: none;
}
.react-flow__edge-textwrapper {
pointer-events: all;
}
.react-flow__edge-text {
pointer-events: none;
user-select: none;
}
.react-flow__connection {
pointer-events: none;
.animated {
stroke-dasharray: 5;
animation: dashdraw 0.5s linear infinite;
}
}
.react-flow__connection-path {
fill: none;
}
.react-flow__nodes {
position: absolute;
width: 100%;
height: 100%;
pointer-events: none;
transform-origin: 0 0;
z-index: 3;
}
.react-flow__node {
position: absolute;
user-select: none;
pointer-events: all;
transform-origin: 0 0;
}
.react-flow__nodesselection {
z-index: 3;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
transform-origin: left top;
pointer-events: none;
&-rect {
position: absolute;
pointer-events: all;
cursor: grab;
}
}
.react-flow__handle {
pointer-events: none;
&.connectable {
pointer-events: all;
}
}
.react-flow__handle-bottom {
top: auto;
left: 50%;
bottom: -4px;
transform: translate(-50%, 0);
}
.react-flow__handle-top {
left: 50%;
top: -4px;
transform: translate(-50%, 0);
}
.react-flow__handle-left {
top: 50%;
left: -4px;
transform: translate(0, -50%);
}
.react-flow__handle-right {
right: -4px;
top: 50%;
transform: translate(0, -50%);
}
.react-flow__edgeupdater {
cursor: move;
pointer-events: all;
}
/* additional components */
.react-flow__background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.react-flow__controls {
position: absolute;
z-index: 5;
bottom: 10px;
left: 10px;
&-button {
width: 24px;
height: 24px;
border: none;
svg {
width: 100%;
}
}
}
.react-flow__minimap {
position: absolute;
z-index: 5;
bottom: 10px;
right: 10px;
}
+157
View File
@@ -0,0 +1,157 @@
.react-flow__selection {
background: rgba(0, 89, 220, 0.08);
border: 1px dotted rgba(0, 89, 220, 0.8);
}
.react-flow__edge {
&.selected {
.react-flow__edge-path {
stroke: #555;
}
}
&.animated path {
stroke-dasharray: 5;
animation: dashdraw 0.5s linear infinite;
}
&.updating {
.react-flow__edge-path {
stroke: #777;
}
}
}
.react-flow__edge-path {
stroke: #b1b1b7;
stroke-width: 1;
}
.react-flow__edge-text {
font-size: 10px;
}
.react-flow__edge-textbg {
fill: white;
}
.react-flow__connection-path {
stroke: #b1b1b7;
stroke-width: 1;
}
.react-flow__node {
cursor: grab;
}
.react-flow__node-default,
.react-flow__node-input,
.react-flow__node-output {
padding: 10px;
border-radius: 3px;
width: 150px;
font-size: 12px;
color: #222;
text-align: center;
border-width: 1px;
border-style: solid;
}
.react-flow__node-default.selectable,
.react-flow__node-input.selectable,
.react-flow__node-output.selectable {
&:hover {
box-shadow: 0 1px 4px 1px rgba(0, 0, 0, 0.08);
}
}
.react-flow__node-input {
background: #fff;
border-color: #0041d0;
&.selected,
&.selected:hover {
box-shadow: 0 0 0 0.5px #0041d0;
}
.react-flow__handle {
background: #0041d0;
}
}
.react-flow__node-default {
background: #fff;
border-color: #1a192b;
&.selected,
&.selected:hover {
box-shadow: 0 0 0 0.5px #1a192b;
}
.react-flow__handle {
background: #1a192b;
}
}
.react-flow__node-output {
background: #fff;
border-color: #ff0072;
&.selected,
&.selected:hover {
box-shadow: 0 0 0 0.5px #ff0072;
}
.react-flow__handle {
background: #ff0072;
}
}
.react-flow__nodesselection-rect {
background: rgba(0, 89, 220, 0.08);
border: 1px dotted rgba(0, 89, 220, 0.8);
}
.react-flow__handle {
position: absolute;
width: 6px;
height: 6px;
background: #555;
border: 1px solid white;
border-radius: 100%;
&.connectable {
cursor: crosshair;
}
}
.react-flow__minimap {
background-color: #fff;
}
.react-flow__controls {
box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.08);
&-button {
background: #fefefe;
border-bottom: 1px solid #eee;
box-sizing: content-box;
display: flex;
justify-content: center;
align-items: center;
width: 16px;
height: 16px;
cursor: pointer;
user-select: none;
padding: 5px;
svg {
max-width: 12px;
max-height: 12px;
}
&:hover {
background: #f4f4f4;
}
}
}
+441
View File
@@ -0,0 +1,441 @@
import { Selection as D3Selection, ZoomBehavior } from 'd3';
import { Component, HTMLAttributes, VNode } from 'vue';
export type ElementId = string;
export type FlowElement<T = any> = Node<T> | Edge<T>;
export type Elements<T = any> = Array<FlowElement<T>>;
export type Transform = [number, number, number];
export enum Position {
Left = 'left',
Top = 'top',
Right = 'right',
Bottom = 'bottom'
}
export interface XYPosition {
x: number;
y: number;
}
export interface Dimensions {
width: number;
height: number;
}
export interface Rect extends Dimensions, XYPosition {}
export interface Box extends XYPosition {
x2: number;
y2: number;
}
export type SnapGrid = [number, number];
export interface Node<T = any> {
id: ElementId;
position: XYPosition;
type?: string;
__rf?: any;
data?: T;
style?: any;
className?: string;
targetPosition?: Position;
sourcePosition?: Position;
isHidden?: boolean;
draggable?: boolean;
selectable?: boolean;
connectable?: boolean;
}
export enum ArrowHeadType {
Arrow = 'arrow',
ArrowClosed = 'arrowclosed'
}
export interface Edge<T = any> {
id: ElementId;
type?: string;
source: ElementId;
target: ElementId;
sourceHandle?: ElementId | null;
targetHandle?: ElementId | null;
label?: string | VNode;
labelStyle?: any;
labelShowBg?: boolean;
labelBgStyle?: any;
labelBgPadding?: [number, number];
labelBgBorderRadius?: number;
style?: any;
animated?: boolean;
arrowHeadType?: ArrowHeadType;
isHidden?: boolean;
data?: T;
className?: string;
}
export enum BackgroundVariant {
Lines = 'lines',
Dots = 'dots'
}
export type HandleType = 'source' | 'target';
export type NodeTypesType = { [key: string]: VNode };
export type EdgeTypesType = NodeTypesType;
export interface SelectionRect extends Rect {
startX: number;
startY: number;
draw: boolean;
}
export interface WrapEdgeProps<T = any> {
id: ElementId;
className?: string;
type: string;
data?: T;
onClick?: (event: MouseEvent, edge: Edge) => void;
onEdgeDoubleClick?: (event: MouseEvent, edge: Edge) => void;
selected: boolean;
animated?: boolean;
label?: string | VNode;
labelStyle?: any;
labelShowBg?: boolean;
labelBgStyle?: any;
labelBgPadding?: [number, number];
labelBgBorderRadius?: number;
style?: any;
arrowHeadType?: ArrowHeadType;
source: ElementId;
target: ElementId;
sourceHandleId: ElementId | null;
targetHandleId: ElementId | null;
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
sourcePosition: Position;
targetPosition: Position;
elementsSelectable?: boolean;
markerEndId?: string;
isHidden?: boolean;
handleEdgeUpdate: boolean;
onConnectEdge: OnConnectFunc;
onContextMenu?: (event: MouseEvent, edge: Edge) => void;
onMouseEnter?: (event: MouseEvent, edge: Edge) => void;
onMouseMove?: (event: MouseEvent, edge: Edge) => void;
onMouseLeave?: (event: MouseEvent, edge: Edge) => void;
edgeUpdaterRadius?: number;
onEdgeUpdateStart?: (event: MouseEvent, edge: Edge) => void;
onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void;
}
export interface EdgeProps<T = any> {
id: ElementId;
source: ElementId;
target: ElementId;
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
selected?: boolean;
animated?: boolean;
sourcePosition: Position;
targetPosition: Position;
label?: string | VNode;
labelStyle?: any;
labelShowBg?: boolean;
labelBgStyle?: any;
labelBgPadding?: [number, number];
labelBgBorderRadius?: number;
style?: any;
arrowHeadType?: ArrowHeadType;
markerEndId?: string;
data?: T;
sourceHandleId?: ElementId | null;
targetHandleId?: ElementId | null;
}
export interface EdgeSmoothStepProps<T = any> extends EdgeProps<T> {
borderRadius?: number;
}
export interface EdgeTextProps extends HTMLAttributes {
x: number;
y: number;
label?: string | VNode;
labelStyle?: any;
labelShowBg?: boolean;
labelBgStyle?: any;
labelBgPadding?: [number, number];
labelBgBorderRadius?: number;
}
export interface NodeProps<T = any> {
id: ElementId;
type: string;
data: T;
selected: boolean;
isConnectable: boolean;
xPos?: number;
yPos?: number;
targetPosition?: Position;
sourcePosition?: Position;
isDragging?: boolean;
}
export interface NodeComponentProps<T = any> {
id: ElementId;
type: string;
data: T;
selected?: boolean;
isConnectable: boolean;
transform?: Transform;
xPos?: number;
yPos?: number;
targetPosition?: Position;
sourcePosition?: Position;
onClick?: (node: Node) => void;
onNodeDoubleClick?: (node: Node) => void;
onMouseEnter?: (node: Node) => void;
onMouseMove?: (node: Node) => void;
onMouseLeave?: (node: Node) => void;
onContextMenu?: (node: Node) => void;
onNodeDragStart?: (node: Node) => void;
onNodeDrag?: (node: Node) => void;
onNodeDragStop?: (node: Node) => void;
style?: any;
isDragging?: boolean;
}
export interface WrapNodeProps<T = any> {
id: ElementId;
type: string;
data: T;
selected: boolean;
scale: number;
xPos: number;
yPos: number;
isSelectable: boolean;
isDraggable: boolean;
isConnectable: boolean;
selectNodesOnDrag: boolean;
onClick?: (event: MouseEvent, node: Node) => void;
onNodeDoubleClick?: (event: MouseEvent, node: Node) => void;
onMouseEnter?: (event: MouseEvent, node: Node) => void;
onMouseMove?: (event: MouseEvent, node: Node) => void;
onMouseLeave?: (event: MouseEvent, node: Node) => void;
onContextMenu?: (event: MouseEvent, node: Node) => void;
onNodeDragStart?: (event: MouseEvent, node: Node) => void;
onNodeDrag?: (event: MouseEvent, node: Node) => void;
onNodeDragStop?: (event: MouseEvent, node: Node) => void;
style?: any;
className?: string;
sourcePosition?: Position;
targetPosition?: Position;
isHidden?: boolean;
isInitialized?: boolean;
snapToGrid?: boolean;
snapGrid?: SnapGrid;
isDragging?: boolean;
resizeObserver: ResizeObserver | null;
}
export type FitViewParams = {
padding?: number;
includeHiddenNodes?: boolean;
minZoom?: number;
maxZoom?: number;
};
export type FlowExportObject<T = any> = {
elements: Elements<T>;
position: [number, number];
zoom: number;
};
export type FitViewFunc = (fitViewOptions?: FitViewParams) => void;
export type ProjectFunc = (position: XYPosition) => XYPosition;
export type ToObjectFunc<T = any> = () => FlowExportObject<T>;
export type OnLoadParams<T = any> = {
zoomIn: () => void;
zoomOut: () => void;
zoomTo: (zoomLevel: number) => void;
fitView: FitViewFunc;
project: ProjectFunc;
getElements: () => Elements<T>;
setTransform: (transform: FlowTransform) => void;
toObject: ToObjectFunc<T>;
};
export type OnLoadFunc<T = any> = (params: OnLoadParams<T>) => void;
export interface Connection {
source: ElementId | null;
target: ElementId | null;
sourceHandle: ElementId | null;
targetHandle: ElementId | null;
}
export enum ConnectionMode {
Strict = 'strict',
Loose = 'loose'
}
export enum ConnectionLineType {
Bezier = 'default',
Straight = 'straight',
Step = 'step',
SmoothStep = 'smoothstep'
}
export type ConnectionLineComponentProps = {
sourceX: number;
sourceY: number;
sourcePosition?: Position;
targetX: number;
targetY: number;
targetPosition?: Position;
connectionLineStyle?: any;
connectionLineType: ConnectionLineType;
};
export type ConnectionLineComponent = Component<ConnectionLineComponentProps>;
export type OnConnectFunc = (connection: Connection) => void;
export type OnConnectStartParams = {
nodeId: ElementId | null;
handleId: ElementId | null;
handleType: HandleType | null;
};
export type OnConnectStartFunc = (event: MouseEvent, params: OnConnectStartParams) => void;
export type OnConnectStopFunc = (event: MouseEvent) => void;
export type OnConnectEndFunc = (event: MouseEvent) => void;
export type SetConnectionId = {
connectionNodeId: ElementId | null;
connectionHandleId: ElementId | null;
connectionHandleType: HandleType | null;
};
export interface HandleElement extends XYPosition, Dimensions {
id?: ElementId | null;
position: Position;
}
export interface HandleProps {
type: HandleType;
position: Position;
isConnectable?: boolean;
onConnect?: OnConnectFunc;
isValidConnection?: (connection: Connection) => boolean;
id?: ElementId;
}
export type NodePosUpdate = {
id: ElementId;
pos: XYPosition;
};
export type NodeDiffUpdate = {
id?: ElementId;
diff?: XYPosition;
isDragging?: boolean;
};
export type FlowTransform = {
x: number;
y: number;
zoom: number;
};
export type TranslateExtent = [[number, number], [number, number]];
export type NodeExtent = TranslateExtent;
export type KeyCode = number | string;
export enum PanOnScrollMode {
Free = 'free',
Vertical = 'vertical',
Horizontal = 'horizontal'
}
export interface ZoomPanHelperFunctions {
zoomIn: () => void;
zoomOut: () => void;
zoomTo: (zoomLevel: number) => void;
transform: (transform: FlowTransform) => void;
fitView: FitViewFunc;
setCenter: (x: number, y: number, zoom?: number) => void;
fitBounds: (bounds: Rect, padding?: number) => void;
project: (position: XYPosition) => XYPosition;
initialized: boolean;
}
export type OnEdgeUpdateFunc<T = any> = (oldEdge: Edge<T>, newConnection: Connection) => void;
export type NodeDimensionUpdate = {
id: ElementId;
nodeElement: HTMLDivElement;
forceUpdate?: boolean;
};
export type InitD3ZoomPayload = {
d3Zoom: ZoomBehavior<Element, unknown>;
d3Selection: D3Selection<Element, unknown, null, undefined>;
d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined;
transform: Transform;
};
export interface ReactFlowState {
width: number;
height: number;
transform: Transform;
nodes: Node[];
edges: Edge[];
selectedElements: Elements | null;
selectedNodesBbox: Rect;
d3Zoom: ZoomBehavior<Element, unknown> | null;
d3Selection: D3Selection<Element, unknown, null, undefined> | null;
d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined;
minZoom: number;
maxZoom: number;
translateExtent: TranslateExtent;
nodeExtent: NodeExtent;
nodesSelectionActive: boolean;
selectionActive: boolean;
userSelectionRect: SelectionRect;
connectionNodeId: ElementId | null;
connectionHandleId: ElementId | null;
connectionHandleType: HandleType | null;
connectionPosition: XYPosition;
connectionMode: ConnectionMode;
snapToGrid: boolean;
snapGrid: SnapGrid;
nodesDraggable: boolean;
nodesConnectable: boolean;
elementsSelectable: boolean;
multiSelectionActive: boolean;
reactFlowVersion: string;
onConnect?: OnConnectFunc;
onConnectStart?: OnConnectStartFunc;
onConnectStop?: OnConnectStopFunc;
onConnectEnd?: OnConnectEndFunc;
}
export type UpdateNodeInternals = (nodeId: ElementId) => void;
+303
View File
@@ -0,0 +1,303 @@
import { Store } from 'pinia';
import { clampPosition, clamp } from './index';
import {
ElementId,
Node,
Edge,
Elements,
Transform,
XYPosition,
Rect,
Box,
Connection,
FlowExportObject,
ReactFlowState,
NodeExtent
} from '../types';
export const isEdge = (element: Node | Connection | Edge): element is Edge =>
'id' in element && 'source' in element && 'target' in element;
export const isNode = (element: Node | Connection | Edge): element is Node =>
'id' in element && !('source' in element) && !('target' in element);
export const getOutgoers = (node: Node, elements: Elements): Node[] => {
if (!isNode(node)) {
return [];
}
const outgoerIds = elements.filter((e) => isEdge(e) && e.source === node.id).map((e) => (e as Edge).target);
return elements.filter((e) => outgoerIds.includes(e.id)) as Node[];
};
export const getIncomers = (node: Node, elements: Elements): Node[] => {
if (!isNode(node)) {
return [];
}
const incomersIds = elements.filter((e) => isEdge(e) && e.target === node.id).map((e) => (e as Edge).source);
return elements.filter((e) => incomersIds.includes(e.id)) as Node[];
};
export const removeElements = (elementsToRemove: Elements, elements: Elements): Elements => {
const nodeIdsToRemove = elementsToRemove.map((n) => n.id);
return elements.filter((element) => {
const edgeElement = element as Edge;
return !(
nodeIdsToRemove.includes(element.id) ||
nodeIdsToRemove.includes(edgeElement.target) ||
nodeIdsToRemove.includes(edgeElement.source)
);
});
};
const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection): ElementId =>
`reactflow__edge-${source}${sourceHandle}-${target}${targetHandle}`;
const connectionExists = (edge: Edge, elements: Elements) => {
return elements.some(
(el) =>
isEdge(el) &&
el.source === edge.source &&
el.target === edge.target &&
(el.sourceHandle === edge.sourceHandle || (!el.sourceHandle && !edge.sourceHandle)) &&
(el.targetHandle === edge.targetHandle || (!el.targetHandle && !edge.targetHandle))
);
};
export const addEdge = (edgeParams: Edge | Connection, elements: Elements): Elements => {
if (!edgeParams.source || !edgeParams.target) {
console.warn("Can't create edge. An edge needs a source and a target.");
return elements;
}
let edge: Edge;
if (isEdge(edgeParams)) {
edge = { ...edgeParams };
} else {
edge = {
...edgeParams,
id: getEdgeId(edgeParams)
} as Edge;
}
if (connectionExists(edge, elements)) {
return elements;
}
return elements.concat(edge);
};
export const updateEdge = (oldEdge: Edge, newConnection: Connection, elements: Elements): Elements => {
if (!newConnection.source || !newConnection.target) {
console.warn("Can't create new edge. An edge needs a source and a target.");
return elements;
}
const foundEdge = elements.find((e) => isEdge(e) && e.id === oldEdge.id) as Edge;
if (!foundEdge) {
console.warn(`The old edge with id=${oldEdge.id} does not exist.`);
return elements;
}
// Remove old edge and create the new edge with parameters of old edge.
const edge = {
...oldEdge,
id: getEdgeId(newConnection),
source: newConnection.source,
target: newConnection.target,
sourceHandle: newConnection.sourceHandle,
targetHandle: newConnection.targetHandle
} as Edge;
return elements.filter((e) => e.id !== oldEdge.id).concat(edge);
};
export const pointToRendererPoint = (
{ x, y }: XYPosition,
[tx, ty, tScale]: Transform,
snapToGrid: boolean,
[snapX, snapY]: [number, number]
): XYPosition => {
const position: XYPosition = {
x: (x - tx) / tScale,
y: (y - ty) / tScale
};
if (snapToGrid) {
return {
x: snapX * Math.round(position.x / snapX),
y: snapY * Math.round(position.y / snapY)
};
}
return position;
};
export const onLoadProject = (currentStore: Store<'revue-flow', ReactFlowState>) => {
return (position: XYPosition): XYPosition => {
return pointToRendererPoint(position, currentStore.transform, currentStore.snapToGrid, currentStore.snapGrid);
};
};
export const parseNode = (node: Node, nodeExtent: NodeExtent): Node => {
return {
...node,
id: node.id.toString(),
type: node.type || 'default',
__rf: {
position: clampPosition(node.position, nodeExtent),
width: null,
height: null,
handleBounds: {},
isDragging: false
}
};
};
export const parseEdge = (edge: Edge): Edge => {
return {
...edge,
source: edge.source.toString(),
target: edge.target.toString(),
sourceHandle: edge.sourceHandle ? edge.sourceHandle.toString() : null,
targetHandle: edge.targetHandle ? edge.targetHandle.toString() : null,
id: edge.id.toString(),
type: edge.type || 'default'
};
};
const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({
x: Math.min(box1.x, box2.x),
y: Math.min(box1.y, box2.y),
x2: Math.max(box1.x2, box2.x2),
y2: Math.max(box1.y2, box2.y2)
});
export const rectToBox = ({ x, y, width, height }: Rect): Box => ({
x,
y,
x2: x + width,
y2: y + height
});
export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
x,
y,
width: x2 - x,
height: y2 - y
});
export const getBoundsofRects = (rect1: Rect, rect2: Rect): Rect =>
boxToRect(getBoundsOfBoxes(rectToBox(rect1), rectToBox(rect2)));
export const getRectOfNodes = (nodes: Node[]): Rect => {
const box = nodes.reduce(
(currBox, { __rf: { position, width, height } = {} }) =>
getBoundsOfBoxes(
currBox,
rectToBox({
...position,
width,
height
})
),
{ x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity }
);
return boxToRect(box);
};
export const graphPosToZoomedPos = ({ x, y }: XYPosition, [tx, ty, tScale]: Transform): XYPosition => ({
x: x * tScale + tx,
y: y * tScale + ty
});
export const getNodesInside = (nodes: Node[], rect: Rect, [tx, ty, tScale]: Transform = [0, 0, 1], partially = false): Node[] => {
const rBox = rectToBox({
x: (rect.x - tx) / tScale,
y: (rect.y - ty) / tScale,
width: rect.width / tScale,
height: rect.height / tScale
});
return nodes.filter(({ __rf: { position, width, height, isDragging } }) => {
const nBox = rectToBox({ ...position, width, height });
const xOverlap = Math.max(0, Math.min(rBox.x2, nBox.x2) - Math.max(rBox.x, nBox.x));
const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y));
const overlappingArea = Math.ceil(xOverlap * yOverlap);
if (width === null || height === null || isDragging) {
// nodes are initialized with width and height = null
return true;
}
if (partially) {
return overlappingArea > 0;
}
const area = width * height;
return overlappingArea >= area;
});
};
export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => {
const nodeIds = nodes.map((node) => node.id);
return edges.filter((edge) => nodeIds.includes(edge.source) || nodeIds.includes(edge.target));
};
const parseElements = (nodes: Node[], edges: Edge[]): Elements => {
return [
...nodes.map((node) => {
const n = { ...node };
n.position = n.__rf.position;
delete n.__rf;
return n;
}),
...edges.map((e) => ({ ...e }))
];
};
export const onLoadGetElements = (currentStore: Store<'revue-flow', ReactFlowState>) => {
return (): Elements => {
return parseElements(currentStore.nodes ?? [], currentStore.edges ?? []);
};
};
export const onLoadToObject = (currentStore: Store<'revue-flow', ReactFlowState>) => {
return (): FlowExportObject => {
return {
elements: parseElements(currentStore.nodes ?? [], currentStore.edges ?? []),
position: [currentStore.transform[0], currentStore.transform[1]],
zoom: currentStore.transform[2]
};
};
};
export const getTransformForBounds = (
bounds: Rect,
width: number,
height: number,
minZoom: number,
maxZoom: number,
padding = 0.1
): Transform => {
const xZoom = width / (bounds.width * (1 + padding));
const yZoom = height / (bounds.height * (1 + padding));
const zoom = Math.min(xZoom, yZoom);
const clampedZoom = clamp(zoom, minZoom, maxZoom);
const boundsCenterX = bounds.x + bounds.width / 2;
const boundsCenterY = bounds.y + bounds.height / 2;
const x = width / 2 - boundsCenterX * clampedZoom;
const y = height / 2 - boundsCenterY * clampedZoom;
return [x, y, clampedZoom];
};
+27
View File
@@ -0,0 +1,27 @@
import { DraggableEvent } from 'react-draggable';
import { MouseEvent as ReactMouseEvent } from 'react';
import { Dimensions, XYPosition, NodeExtent } from '../types';
export const isInputDOMNode = (e: ReactMouseEvent | DraggableEvent | KeyboardEvent) => {
const target = e?.target as HTMLElement;
return (
['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].includes(target?.nodeName) || target?.hasAttribute('contenteditable')
);
};
export const getDimensions = (node: HTMLDivElement): Dimensions => ({
width: node.offsetWidth,
height: node.offsetHeight,
});
export const clamp = (val: number, min: number = 0, max: number = 1): number => Math.min(Math.max(val, min), max);
export const clampPosition = (position: XYPosition, extent: NodeExtent) => ({
x: clamp(position.x, extent[0][0], extent[1][0]),
y: clamp(position.y, extent[0][1], extent[1][1]),
});
export const getHostForElement = (element: HTMLElement): Document | ShadowRoot =>
(element.getRootNode?.() as Document | ShadowRoot) || window?.document;
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />