Merge branch 'main' into refactor/promised-panzoom
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { CoordinateExtent, HandleType } from './types';
|
||||
|
||||
// @todo: update URLs to xyflow
|
||||
export const errorMessages = {
|
||||
error001: () =>
|
||||
'[React Flow]: Seems like you have not used zustand provider as an ancestor. Help: https://reactflow.dev/error#001',
|
||||
@@ -23,10 +22,10 @@ export const errorMessages = {
|
||||
error011: (edgeType: string) => `Edge type "${edgeType}" not found. Using fallback type "default".`,
|
||||
error012: (id: string) =>
|
||||
`Node with id "${id}" does not exist, it may have been removed. This can happen when a node is deleted before the "onNodeClick" handler is called.`,
|
||||
error013: (lib: string = 'react') =>
|
||||
`It seems that you haven't loaded the styles. Please import '@xyflow/${lib}/dist/style.css' or base.css to make sure everything is working properly.`,
|
||||
};
|
||||
|
||||
export const internalsSymbol = Symbol.for('internals');
|
||||
|
||||
export const infiniteExtent: CoordinateExtent = [
|
||||
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||
|
||||
@@ -5,3 +5,4 @@ export * from './xydrag';
|
||||
export * from './xyhandle';
|
||||
export * from './xyminimap';
|
||||
export * from './xypanzoom';
|
||||
export * from './xyresizer';
|
||||
|
||||
@@ -1,33 +1,38 @@
|
||||
.xy-flow {
|
||||
--node-border-default: 1px solid #bbb;
|
||||
--node-border-selected-default: 1px solid #555;
|
||||
--xy-node-border-default: 1px solid #bbb;
|
||||
--xy-node-border-selected-default: 1px solid #555;
|
||||
|
||||
--handle-background-color-default: #333;
|
||||
--xy-handle-background-color-default: #333;
|
||||
|
||||
--selection-background-color-default: rgba(150, 150, 180, 0.1);
|
||||
--selection-border-default: 1px dotted rgba(155, 155, 155, 0.8);
|
||||
--xy-selection-background-color-default: rgba(150, 150, 180, 0.1);
|
||||
--xy-selection-border-default: 1px dotted rgba(155, 155, 155, 0.8);
|
||||
}
|
||||
|
||||
.xy-flow.dark {
|
||||
--xy-node-color-default: #f8f8f8;
|
||||
}
|
||||
|
||||
.xy-flow__handle {
|
||||
background-color: var(--handle-background-color, var(--handle-background-color-default));
|
||||
background-color: var(--xy-handle-background-color, var(--xy-handle-background-color-default));
|
||||
}
|
||||
|
||||
.xy-flow__node-input,
|
||||
.xy-flow__node-default,
|
||||
.xy-flow__node-output,
|
||||
.xy-flow__node-group {
|
||||
border: var(--node-border, var(--node-border-default));
|
||||
border: var(--xy-node-border, var(--xy-node-border-default));
|
||||
color: var(--xy-node-color, var(--xy-node-color-default));
|
||||
|
||||
&.selected,
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
border: var(--node-border-selected, var(--node-border-selected-default));
|
||||
border: var(--xy-node-border-selected, var(--xy-node-border-selected-default));
|
||||
}
|
||||
}
|
||||
|
||||
.xy-flow__nodesselection-rect,
|
||||
.xy-flow__selection {
|
||||
background: var(--selection-background-color, var(--selection-background-color-default));
|
||||
border: var(--selection-border, var(--selection-border-default));
|
||||
background: var(--xy-selection-background-color, var(--xy-selection-background-color-default));
|
||||
border: var(--xy-selection-border, var(--xy-selection-border-default));
|
||||
}
|
||||
|
||||
@@ -1,20 +1,61 @@
|
||||
/* these are the necessary styles for React/Svelte Flow, they get used by base.css and style.css */
|
||||
|
||||
.xy-flow {
|
||||
--edge-stroke-default: #b1b1b7;
|
||||
--edge-stroke-width-default: 1;
|
||||
--edge-stroke-selected-default: #555;
|
||||
direction: ltr;
|
||||
|
||||
--connectionline-stroke-default: #b1b1b7;
|
||||
--connectionline-stroke-width-default: 1;
|
||||
--xy-edge-stroke-default: #b1b1b7;
|
||||
--xy-edge-stroke-width-default: 1;
|
||||
--xy-edge-stroke-selected-default: #555;
|
||||
|
||||
--attribution-background-color-default: rgba(255, 255, 255, 0.5);
|
||||
--xy-connectionline-stroke-default: #b1b1b7;
|
||||
--xy-connectionline-stroke-width-default: 1;
|
||||
|
||||
--minimap-background-color-default: #fff;
|
||||
--xy-attribution-background-color-default: rgba(255, 255, 255, 0.5);
|
||||
|
||||
--background-pattern-dot-color-default: #91919a;
|
||||
--background-pattern-line-color-default: #eee;
|
||||
--background-pattern-cross-color-default: #e2e2e2;
|
||||
--xy-minimap-background-color-default: #fff;
|
||||
--xy-minimap-mask-background-color-default: rgb(240, 240, 240, 0.6);
|
||||
--xy-minimap-mask-stroke-color-default: transparent;
|
||||
--xy-minimap-mask-stroke-width-default: 1;
|
||||
--xy-minimap-node-background-color-default: #e2e2e2;
|
||||
--xy-minimap-node-stroke-color-default: transparent;
|
||||
--xy-minimap-node-stroke-width-default: 2;
|
||||
|
||||
--xy-background-color-default: transparent;
|
||||
--xy-background-pattern-dots-color-default: #91919a;
|
||||
--xy-background-pattern-lines-color-default: #eee;
|
||||
--xy-background-pattern-cross-color-default: #e2e2e2;
|
||||
}
|
||||
|
||||
.xy-flow.dark {
|
||||
--xy-edge-stroke-default: #3e3e3e;
|
||||
--xy-edge-stroke-width-default: 1;
|
||||
--xy-edge-stroke-selected-default: #727272;
|
||||
|
||||
--xy-connectionline-stroke-default: #b1b1b7;
|
||||
--xy-connectionline-stroke-width-default: 1;
|
||||
|
||||
--xy-attribution-background-color-default: rgba(150, 150, 150, 0.25);
|
||||
|
||||
--xy-minimap-background-color-default: #141414;
|
||||
--xy-minimap-mask-background-color-default: rgb(60, 60, 60, 0.6);
|
||||
--xy-minimap-mask-stroke-color-default: transparent;
|
||||
--xy-minimap-mask-stroke-width-default: 1;
|
||||
--xy-minimap-node-background-color-default: #2b2b2b;
|
||||
--xy-minimap-node-stroke-color-default: transparent;
|
||||
--xy-minimap-node-stroke-width-default: 2;
|
||||
|
||||
--xy-background-color-default: #141414;
|
||||
--xy-background-pattern-dots-color-default: #777;
|
||||
--xy-background-pattern-lines-color-default: #777;
|
||||
--xy-background-pattern-cross-color-default: #777;
|
||||
}
|
||||
|
||||
.xy-flow {
|
||||
background-color: var(--xy-background-color, var(--xy-background-color-default));
|
||||
}
|
||||
|
||||
.xy-flow__background {
|
||||
background-color: var(--xy-background-color, var(--xy-background-color-props, var(--xy-background-color-default)));
|
||||
}
|
||||
|
||||
.xy-flow__container {
|
||||
@@ -27,14 +68,17 @@
|
||||
|
||||
.xy-flow__pane {
|
||||
z-index: 1;
|
||||
cursor: grab;
|
||||
|
||||
&.selection {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
cursor: grabbing;
|
||||
&.draggable {
|
||||
cursor: grab;
|
||||
|
||||
&.dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,26 +101,34 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.xy-flow .xy-flow__edges {
|
||||
pointer-events: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.xy-flow__edge-path {
|
||||
stroke: var(--edge-stroke, var(--edge-stroke-default));
|
||||
stroke-width: var(--edge-stroke-width, var(--edge-stroke-width-default));
|
||||
stroke: var(--xy-edge-stroke, var(--xy-edge-stroke-default));
|
||||
stroke-width: var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.xy-flow__connection-path {
|
||||
stroke: var(--connectionline-stroke, var(--connectionline-stroke-default));
|
||||
stroke-width: var(--connectionline-stroke-width, var(--connectionline-stroke-width-default));
|
||||
stroke: var(--xy-connectionline-stroke, var(--xy-connectionline-stroke-default));
|
||||
stroke-width: var(--xy-connectionline-stroke-width, var(--xy-connectionline-stroke-width-default));
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.xy-flow__edges {
|
||||
position: absolute;
|
||||
|
||||
svg {
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.xy-flow__edge {
|
||||
pointer-events: visibleStroke;
|
||||
cursor: pointer;
|
||||
|
||||
&.selectable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.animated path {
|
||||
stroke-dasharray: 5;
|
||||
@@ -99,25 +151,20 @@
|
||||
}
|
||||
|
||||
&.selected .xy-flow__edge-path,
|
||||
&:focus .xy-flow__edge-path,
|
||||
&:focus-visible .xy-flow__edge-path {
|
||||
stroke: var(--edge-stroke-selected, var(--edge-stroke-selected-default));
|
||||
&.selectable:focus .xy-flow__edge-path,
|
||||
&.selectable:focus-visible .xy-flow__edge-path {
|
||||
stroke: var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default));
|
||||
}
|
||||
|
||||
&-textwrapper {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
&-textbg {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.xy-flow__edge-text {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.xy-flow__connection {
|
||||
pointer-events: none;
|
||||
|
||||
@@ -127,7 +174,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.xy-flow__connectionline {
|
||||
svg.xy-flow__connectionline {
|
||||
z-index: 1001;
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
@@ -144,16 +191,19 @@
|
||||
pointer-events: all;
|
||||
transform-origin: 0 0;
|
||||
box-sizing: border-box;
|
||||
cursor: grab;
|
||||
cursor: default;
|
||||
|
||||
&.dragging {
|
||||
cursor: grabbing;
|
||||
&.selectable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* only used in Svelte Flow, should we remove it here? */
|
||||
&.draggable {
|
||||
cursor: grab;
|
||||
pointer-events: all;
|
||||
|
||||
&.dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +225,10 @@
|
||||
min-width: 5px;
|
||||
min-height: 5px;
|
||||
|
||||
&.connectingfrom {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
&.connectionindicator {
|
||||
pointer-events: all;
|
||||
cursor: crosshair;
|
||||
@@ -183,26 +237,26 @@
|
||||
&-bottom {
|
||||
top: auto;
|
||||
left: 50%;
|
||||
bottom: -4px;
|
||||
transform: translate(-50%, 0);
|
||||
bottom: 0;
|
||||
transform: translate(-50%, 50%);
|
||||
}
|
||||
|
||||
&-top {
|
||||
top: 0;
|
||||
left: 50%;
|
||||
top: -4px;
|
||||
transform: translate(-50%, 0);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
&-left {
|
||||
top: 50%;
|
||||
left: -4px;
|
||||
transform: translate(0, -50%);
|
||||
left: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
&-right {
|
||||
right: -4px;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
right: 0;
|
||||
transform: translate(50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,7 +294,7 @@
|
||||
|
||||
.xy-flow__attribution {
|
||||
font-size: 10px;
|
||||
background: var(--attribution-background-color, var(--attribution-background-color-default));
|
||||
background: var(--xy-attribution-background-color, var(--xy-attribution-background-color-default));
|
||||
padding: 2px 3px;
|
||||
margin: 0;
|
||||
|
||||
@@ -266,40 +320,92 @@
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.xy-flow__viewport-portal {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.xy-flow__minimap {
|
||||
background: var(--minimap-background-color, var(--minimap-background-color-default));
|
||||
background: var(
|
||||
--xy-minimap-background-color-props,
|
||||
var(--xy-minimap-background-color, var(--xy-minimap-background-color-default))
|
||||
);
|
||||
|
||||
&-svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&-mask {
|
||||
fill: var(
|
||||
--xy-minimap-mask-background-color-props,
|
||||
var(--xy-minimap-mask-background-color, var(--xy-minimap-mask-background-color-default))
|
||||
);
|
||||
stroke: var(
|
||||
--xy-minimap-mask-stroke-color-props,
|
||||
var(--xy-minimap-mask-stroke-color, var(--xy-minimap-mask-stroke-color-default))
|
||||
);
|
||||
stroke-width: var(
|
||||
--xy-minimap-mask-stroke-width-props,
|
||||
var(--xy-minimap-mask-stroke-width, var(--xy-minimap-mask-stroke-width-default))
|
||||
);
|
||||
}
|
||||
|
||||
&-node {
|
||||
fill: var(
|
||||
--xy-minimap-node-background-color-props,
|
||||
var(--xy-minimap-node-background-color, var(--xy-minimap-node-background-color-default))
|
||||
);
|
||||
stroke: var(
|
||||
--xy-minimap-node-stroke-color-props,
|
||||
var(--xy-minimap-node-stroke-color, var(--xy-minimap-node-stroke-color-default))
|
||||
);
|
||||
stroke-width: var(
|
||||
--xy-minimap-node-stroke-width-props,
|
||||
var(--xy-minimap-node-stroke-width, var(--xy-minimap-node-stroke-width-default))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.xy-flow__background {
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
background-color: var(--background-color-props, 'transparent');
|
||||
}
|
||||
|
||||
.xy-flow__background-pattern {
|
||||
&.dots {
|
||||
fill: var(
|
||||
--background-pattern-color-props,
|
||||
var(--background-pattern-color, var(--background-pattern-dot-color-default))
|
||||
--xy-background-pattern-color-props,
|
||||
var(--xy-background-pattern-color, var(--xy-background-pattern-dots-color-default))
|
||||
);
|
||||
}
|
||||
|
||||
&.lines {
|
||||
stroke: var(
|
||||
--background-pattern-color-props,
|
||||
var(--background-pattern-color, var(--background-pattern-line-color-default))
|
||||
--xy-background-pattern-color-props,
|
||||
var(--xy-background-pattern-color, var(--xy-background-pattern-lines-color-default))
|
||||
);
|
||||
}
|
||||
|
||||
&.cross {
|
||||
stroke: var(
|
||||
--background-pattern-color-props,
|
||||
var(--background-pattern-color, var(--background-pattern-cross-color-default))
|
||||
--xy-background-pattern-color-props,
|
||||
var(--xy-background-pattern-color, var(--xy-background-pattern-cross-color-default))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.xy-flow__controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -312,6 +418,7 @@
|
||||
width: 100%;
|
||||
max-width: 12px;
|
||||
max-height: 12px;
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.xy-flow {
|
||||
--xy-resize-background-color-default: #3367d9;
|
||||
}
|
||||
|
||||
.xy-flow__resize-control {
|
||||
position: absolute;
|
||||
}
|
||||
@@ -28,7 +32,7 @@
|
||||
height: 4px;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 1px;
|
||||
background-color: #3367d9;
|
||||
background-color: var(--xy-resize-background-color, var(--xy-resize-background-color-default));
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@@ -63,7 +67,7 @@
|
||||
|
||||
/* line styles */
|
||||
.xy-flow__resize-control.line {
|
||||
border-color: #3367d9;
|
||||
border-color: var(--xy-resize-background-color, var(--xy-resize-background-color-default));
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,52 @@
|
||||
.xy-flow {
|
||||
--node-color-default: inherit;
|
||||
--node-border-default: 1px solid #1a192b;
|
||||
--node-background-color-default: #fff;
|
||||
--node-group-background-color-default: rgba(240, 240, 240, 0.25);
|
||||
--node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, 0.08);
|
||||
--node-boxshadow-selected-default: 0 0 0 0.5px #1a192b;
|
||||
--xy-node-color-default: inherit;
|
||||
--xy-node-border-default: 1px solid #1a192b;
|
||||
--xy-node-background-color-default: #fff;
|
||||
--xy-node-group-background-color-default: rgba(240, 240, 240, 0.25);
|
||||
--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, 0.08);
|
||||
--xy-node-boxshadow-selected-default: 0 0 0 0.5px #1a192b;
|
||||
--xy-node-border-radius-default: 3px;
|
||||
|
||||
--handle-background-color-default: #1a192b;
|
||||
--handle-border-color-default: #fff;
|
||||
--xy-handle-background-color-default: #1a192b;
|
||||
--xy-handle-border-color-default: #fff;
|
||||
|
||||
--selection-background-color-default: rgba(0, 89, 220, 0.08);
|
||||
--selection-border-default: 1px dotted rgba(0, 89, 220, 0.8);
|
||||
--xy-selection-background-color-default: rgba(0, 89, 220, 0.08);
|
||||
--xy-selection-border-default: 1px dotted rgba(0, 89, 220, 0.8);
|
||||
|
||||
--controls-button-background-color-default: #fefefe;
|
||||
--controls-button-background-color-hover-default: #f4f4f4;
|
||||
--controls-button-color-default: inherit;
|
||||
--controls-button-color-hover-default: inherit;
|
||||
--controls-button-border-color-default: #eee;
|
||||
--controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08);
|
||||
--xy-controls-button-background-color-default: #fefefe;
|
||||
--xy-controls-button-background-color-hover-default: #f4f4f4;
|
||||
--xy-controls-button-color-default: inherit;
|
||||
--xy-controls-button-color-hover-default: inherit;
|
||||
--xy-controls-button-border-color-default: #eee;
|
||||
--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08);
|
||||
|
||||
--xy-edge-label-background-color-default: #ffffff;
|
||||
--xy-edge-label-color-default: inherit;
|
||||
}
|
||||
|
||||
.xy-flow.dark {
|
||||
--xy-node-color-default: #f8f8f8;
|
||||
--xy-node-border-default: 1px solid #3c3c3c;
|
||||
--xy-node-background-color-default: #1e1e1e;
|
||||
--xy-node-group-background-color-default: rgba(240, 240, 240, 0.25);
|
||||
--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, 0.08);
|
||||
--xy-node-boxshadow-selected-default: 0 0 0 0.5px #999;
|
||||
|
||||
--xy-handle-background-color-default: #bebebe;
|
||||
--xy-handle-border-color-default: #1e1e1e;
|
||||
|
||||
--xy-selection-background-color-default: rgba(200, 200, 220, 0.08);
|
||||
--xy-selection-border-default: 1px dotted rgba(200, 200, 220, 0.8);
|
||||
|
||||
--xy-controls-button-background-color-default: #2b2b2b;
|
||||
--xy-controls-button-background-color-hover-default: #3e3e3e;
|
||||
--xy-controls-button-color-default: #f8f8f8;
|
||||
--xy-controls-button-color-hover-default: #fff;
|
||||
--xy-controls-button-border-color-default: #5b5b5b;
|
||||
--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08);
|
||||
|
||||
--xy-edge-label-background-color-default: #141414;
|
||||
--xy-edge-label-color-default: #f8f8f8;
|
||||
}
|
||||
|
||||
.xy-flow__edge {
|
||||
@@ -44,35 +73,35 @@
|
||||
.xy-flow__node-output,
|
||||
.xy-flow__node-group {
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
border-radius: var(--xy-node-border-radius, var(--xy-node-border-radius-default));
|
||||
width: 150px;
|
||||
font-size: 12px;
|
||||
color: var(--node-color, var(--node-color-default));
|
||||
color: var(--xy-node-color, var(--xy-node-color-default));
|
||||
text-align: center;
|
||||
border: var(--node-border, var(--node-border-default));
|
||||
background-color: var(--node-background-color, var(--node-background-color-default));
|
||||
border: var(--xy-node-border, var(--xy-node-border-default));
|
||||
background-color: var(--xy-node-background-color, var(--xy-node-background-color-default));
|
||||
|
||||
&.selectable {
|
||||
&:hover {
|
||||
box-shadow: var(--node-boxshadow-hover, var(--node-boxshadow-hover-default));
|
||||
box-shadow: var(--xy-node-boxshadow-hover, var(--xy-node-boxshadow-hover-default));
|
||||
}
|
||||
|
||||
&.selected,
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
box-shadow: var(--node-boxshadow-selected, var(--node-boxshadow-selected-default));
|
||||
box-shadow: var(--xy-node-boxshadow-selected, var(--xy-node-boxshadow-selected-default));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.xy-flow__node-group {
|
||||
background-color: var(--node-group-background-color, var(--node-group-background-color-default));
|
||||
background-color: var(--xy-node-group-background-color, var(--xy-node-group-background-color-default));
|
||||
}
|
||||
|
||||
.xy-flow__nodesselection-rect,
|
||||
.xy-flow__selection {
|
||||
background: var(--selection-background-color, var(--selection-background-color-default));
|
||||
border: var(--selection-border, var(--selection-border-default));
|
||||
background: var(--xy-selection-background-color, var(--xy-selection-background-color-default));
|
||||
border: var(--xy-selection-border, var(--xy-selection-border-default));
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
@@ -83,34 +112,37 @@
|
||||
.xy-flow__handle {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: var(--handle-background-color, var(--handle-background-color-default));
|
||||
border: 1px solid var(--handle-border-color, var(--handle-border-color-default));
|
||||
background-color: var(--xy-handle-background-color, var(--xy-handle-background-color-default));
|
||||
border: 1px solid var(--xy-handle-border-color, var(--xy-handle-border-color-default));
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.xy-flow__controls {
|
||||
box-shadow: var(--controls-box-shadow, var(--controls-box-shadow-default));
|
||||
box-shadow: var(--xy-controls-box-shadow, var(--xy-controls-box-shadow-default));
|
||||
|
||||
&-button {
|
||||
border: none;
|
||||
background: var(--controls-button-background-color, var(--controls-button-background-color-default));
|
||||
background: var(--xy-controls-button-background-color, var(--xy-controls-button-background-color-default));
|
||||
border-bottom: 1px solid
|
||||
var(
|
||||
--controls-button-border-color-props,
|
||||
var(--controls-button-border-color, var(--controls-button-border-color-default))
|
||||
--xy-controls-button-border-color-props,
|
||||
var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default))
|
||||
);
|
||||
color: var(--controls-button-color-props, var(--controls-button-color, var(--controls-button-color-default)));
|
||||
color: var(
|
||||
--xy-controls-button-color-props,
|
||||
var(--xy-controls-button-color, var(--xy-controls-button-color-default))
|
||||
);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background: var(
|
||||
--controls-button-background-color-hover-props,
|
||||
var(--controls-button-background-color-hover, var(--controls-button-background-color-hover-default))
|
||||
--xy-controls-button-background-color-hover-props,
|
||||
var(--xy-controls-button-background-color-hover, var(--xy-controls-button-background-color-hover-default))
|
||||
);
|
||||
color: var(
|
||||
--controls-button-color-hover-props,
|
||||
var(--controls-button-hover-color, var(--controls-button-hover-color-default))
|
||||
--xy-controls-button-color-hover-props,
|
||||
var(--xy-controls-button-color-hover, var(--xy-controls-button-color-hover-default))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -122,4 +154,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-button:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { XYPosition, Dimensions, NodeBase, EdgeBase } from '.';
|
||||
|
||||
export type NodeDimensionChange = {
|
||||
id: string;
|
||||
type: 'dimensions';
|
||||
dimensions?: Dimensions;
|
||||
/* if this is true, the node is currently being resized via the NodeResizer */
|
||||
resizing?: boolean;
|
||||
/* if this is true, we will set width and height of the node and not just the measured dimensions */
|
||||
setAttributes?: boolean;
|
||||
};
|
||||
|
||||
export type NodePositionChange = {
|
||||
id: string;
|
||||
type: 'position';
|
||||
position?: XYPosition;
|
||||
positionAbsolute?: XYPosition;
|
||||
dragging?: boolean;
|
||||
};
|
||||
|
||||
export type NodeSelectionChange = {
|
||||
id: string;
|
||||
type: 'select';
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
export type NodeRemoveChange = {
|
||||
id: string;
|
||||
type: 'remove';
|
||||
};
|
||||
|
||||
export type NodeAddChange<NodeType extends NodeBase = NodeBase> = {
|
||||
item: NodeType;
|
||||
type: 'add';
|
||||
};
|
||||
|
||||
export type NodeReplaceChange<NodeType extends NodeBase = NodeBase> = {
|
||||
id: string;
|
||||
item: NodeType;
|
||||
type: 'replace';
|
||||
};
|
||||
|
||||
/**
|
||||
* Union type of all possible node changes.
|
||||
* @public
|
||||
*/
|
||||
export type NodeChange<NodeType extends NodeBase = NodeBase> =
|
||||
| NodeDimensionChange
|
||||
| NodePositionChange
|
||||
| NodeSelectionChange
|
||||
| NodeRemoveChange
|
||||
| NodeAddChange<NodeType>
|
||||
| NodeReplaceChange<NodeType>;
|
||||
|
||||
export type EdgeSelectionChange = NodeSelectionChange;
|
||||
export type EdgeRemoveChange = NodeRemoveChange;
|
||||
export type EdgeAddChange<EdgeType extends EdgeBase = EdgeBase> = {
|
||||
item: EdgeType;
|
||||
type: 'add';
|
||||
};
|
||||
|
||||
export type EdgeReplaceChange<EdgeType extends EdgeBase = EdgeBase> = {
|
||||
id: string;
|
||||
item: EdgeType;
|
||||
type: 'replace';
|
||||
};
|
||||
|
||||
export type EdgeChange<EdgeType extends EdgeBase = EdgeBase> =
|
||||
| EdgeSelectionChange
|
||||
| EdgeRemoveChange
|
||||
| EdgeAddChange<EdgeType>
|
||||
| EdgeReplaceChange<EdgeType>;
|
||||
@@ -1,25 +1,44 @@
|
||||
import { Position } from './utils';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type EdgeBase<EdgeData = any> = {
|
||||
export type EdgeBase<
|
||||
EdgeData extends Record<string, unknown> = Record<string, unknown>,
|
||||
EdgeType extends string | undefined = string | undefined
|
||||
> = {
|
||||
/** Unique id of an edge */
|
||||
id: string;
|
||||
type?: string;
|
||||
/** Type of an edge defined in edgeTypes */
|
||||
type?: EdgeType;
|
||||
/** Id of source node */
|
||||
source: string;
|
||||
/** Id of target node */
|
||||
target: string;
|
||||
/** Id of source handle
|
||||
* only needed if there are multiple handles per node
|
||||
*/
|
||||
sourceHandle?: string | null;
|
||||
/** Id of target handle
|
||||
* only needed if there are multiple handles per node
|
||||
*/
|
||||
targetHandle?: string | null;
|
||||
animated?: boolean;
|
||||
hidden?: boolean;
|
||||
deletable?: boolean;
|
||||
selectable?: boolean;
|
||||
/** Arbitrary data passed to an edge */
|
||||
data?: EdgeData;
|
||||
selected?: boolean;
|
||||
/** Set the marker on the beginning of an edge
|
||||
* @example 'arrow', 'arrowclosed' or custom marker
|
||||
*/
|
||||
markerStart?: EdgeMarkerType;
|
||||
/** Set the marker on the end of an edge
|
||||
* @example 'arrow', 'arrowclosed' or custom marker
|
||||
*/
|
||||
markerEnd?: EdgeMarkerType;
|
||||
zIndex?: number;
|
||||
ariaLabel?: string;
|
||||
/** Padding around the edge where interaction is still possible */
|
||||
interactionWidth?: number;
|
||||
focusable?: boolean;
|
||||
};
|
||||
|
||||
export type SmoothStepPathOptions = {
|
||||
@@ -27,13 +46,17 @@ export type SmoothStepPathOptions = {
|
||||
borderRadius?: number;
|
||||
};
|
||||
|
||||
export type StepPathOptions = {
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
export type BezierPathOptions = {
|
||||
curvature?: number;
|
||||
};
|
||||
|
||||
export type DefaultEdgeOptionsBase<EdgeType extends EdgeBase> = Omit<
|
||||
EdgeType,
|
||||
'id' | 'source' | 'target' | 'sourceHandle' | 'targetHandle' | 'sourceNode' | 'targetNode'
|
||||
'id' | 'source' | 'target' | 'sourceHandle' | 'targetHandle' | 'selected'
|
||||
>;
|
||||
|
||||
export enum ConnectionLineType {
|
||||
@@ -73,3 +96,5 @@ export type EdgePosition = {
|
||||
sourcePosition: Position;
|
||||
targetPosition: Position;
|
||||
};
|
||||
|
||||
export type EdgeLookup<EdgeType extends EdgeBase = EdgeBase> = Map<string, EdgeType>;
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type { D3DragEvent, Selection as D3Selection, SubjectPosition, ZoomBehavior } from 'd3';
|
||||
import type { Selection as D3Selection } from 'd3-selection';
|
||||
import type { D3DragEvent, SubjectPosition } from 'd3-drag';
|
||||
import type { ZoomBehavior } from 'd3-zoom';
|
||||
// this is needed for the Selection type to include the transition function :/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import type { Transition } from 'd3-transition';
|
||||
|
||||
import type { XYPosition, Rect } from './utils';
|
||||
import type { NodeBase, NodeDragItem, NodeOrigin } from './nodes';
|
||||
import type { ConnectingHandle, HandleType } from './handles';
|
||||
import type { XYPosition, Rect, Position } from './utils';
|
||||
import type { InternalNodeBase, NodeBase, NodeDragItem } from './nodes';
|
||||
import type { Handle, HandleType } from './handles';
|
||||
import { PanZoomInstance } from './panzoom';
|
||||
import { EdgeBase } from '..';
|
||||
|
||||
@@ -22,13 +27,15 @@ export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => vo
|
||||
export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => void;
|
||||
|
||||
export type Connection = {
|
||||
source: string | null;
|
||||
target: string | null;
|
||||
source: string;
|
||||
target: string;
|
||||
sourceHandle: string | null;
|
||||
targetHandle: string | null;
|
||||
};
|
||||
|
||||
export type ConnectionStatus = 'valid' | 'invalid';
|
||||
export type HandleConnection = Connection & {
|
||||
edgeId: string;
|
||||
};
|
||||
|
||||
export enum ConnectionMode {
|
||||
Strict = 'strict',
|
||||
@@ -48,22 +55,21 @@ export type OnConnectEnd = (event: MouseEvent | TouchEvent) => void;
|
||||
export type IsValidConnection = (edge: EdgeBase | Connection) => boolean;
|
||||
|
||||
export type FitViewParamsBase<NodeType extends NodeBase> = {
|
||||
nodes: NodeType[];
|
||||
nodeLookup: Map<string, InternalNodeBase<NodeType>>;
|
||||
width: number;
|
||||
height: number;
|
||||
panZoom: PanZoomInstance;
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
nodeOrigin?: NodeOrigin;
|
||||
};
|
||||
|
||||
export type FitViewOptionsBase<NodeType extends NodeBase> = {
|
||||
export type FitViewOptionsBase<NodeType extends NodeBase = NodeBase> = {
|
||||
padding?: number;
|
||||
includeHiddenNodes?: boolean;
|
||||
minZoom?: number;
|
||||
maxZoom?: number;
|
||||
duration?: number;
|
||||
nodes?: (NodeType | { id: NodeType['id'] })[];
|
||||
nodes?: (NodeType | { id: string })[];
|
||||
};
|
||||
|
||||
export type Viewport = {
|
||||
@@ -100,7 +106,7 @@ export type D3ZoomInstance = ZoomBehavior<Element, unknown>;
|
||||
export type D3SelectionInstance = D3Selection<Element, unknown, null, undefined>;
|
||||
export type D3ZoomHandler = (this: Element, event: any, d: unknown) => void;
|
||||
|
||||
export type UpdateNodeInternals = (nodeId: string) => void;
|
||||
export type UpdateNodeInternals = (nodeId: string | string[]) => void;
|
||||
|
||||
export type PanelPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
||||
|
||||
@@ -123,16 +129,65 @@ export type SelectionRect = Rect & {
|
||||
|
||||
export type OnError = (id: string, message: string) => void;
|
||||
|
||||
export type UpdateNodePositions = (
|
||||
dragItems: NodeDragItem[] | NodeBase[],
|
||||
positionChanged?: boolean,
|
||||
dragging?: boolean
|
||||
) => void;
|
||||
export type UpdateNodePositions = (dragItems: Map<string, NodeDragItem | InternalNodeBase>, dragging?: boolean) => void;
|
||||
export type PanBy = (delta: XYPosition) => boolean;
|
||||
|
||||
export type UpdateConnection = (params: {
|
||||
connectionPosition: XYPosition | null;
|
||||
connectionStatus: ConnectionStatus | null;
|
||||
connectionStartHandle: ConnectingHandle | null;
|
||||
connectionEndHandle: ConnectingHandle | null;
|
||||
}) => void;
|
||||
export const initialConnection: NoConnection = {
|
||||
inProgress: false,
|
||||
isValid: null,
|
||||
from: null,
|
||||
fromHandle: null,
|
||||
fromPosition: null,
|
||||
fromNode: null,
|
||||
to: null,
|
||||
toHandle: null,
|
||||
toPosition: null,
|
||||
toNode: null,
|
||||
};
|
||||
|
||||
export type NoConnection = {
|
||||
inProgress: false;
|
||||
isValid: null;
|
||||
|
||||
from: null;
|
||||
fromHandle: null;
|
||||
fromPosition: null;
|
||||
fromNode: null;
|
||||
|
||||
to: null;
|
||||
toHandle: null;
|
||||
toPosition: null;
|
||||
toNode: null;
|
||||
};
|
||||
|
||||
export type ConnectionInProgress = {
|
||||
inProgress: true;
|
||||
isValid: boolean | null;
|
||||
|
||||
from: XYPosition;
|
||||
fromHandle: Handle;
|
||||
fromPosition: Position;
|
||||
fromNode: NodeBase;
|
||||
|
||||
to: XYPosition;
|
||||
toHandle: Handle | null;
|
||||
toPosition: Position;
|
||||
toNode: NodeBase | null;
|
||||
};
|
||||
|
||||
export type ConnectionState = ConnectionInProgress | NoConnection;
|
||||
|
||||
export type UpdateConnection = (params: ConnectionState) => void;
|
||||
|
||||
export type ColorModeClass = 'light' | 'dark';
|
||||
export type ColorMode = ColorModeClass | 'system';
|
||||
|
||||
export type ConnectionLookup = Map<string, Map<string, HandleConnection>>;
|
||||
|
||||
export type OnBeforeDeleteBase<NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase> = ({
|
||||
nodes,
|
||||
edges,
|
||||
}: {
|
||||
nodes: NodeType[];
|
||||
edges: EdgeType[];
|
||||
}) => Promise<boolean | { nodes: NodeType[]; edges: EdgeType[] }>;
|
||||
|
||||
@@ -1,38 +1,40 @@
|
||||
import type { Position, OnConnect, IsValidConnection } from '.';
|
||||
import type { Position, IsValidConnection } from '.';
|
||||
|
||||
export type HandleType = 'source' | 'target';
|
||||
|
||||
export type HandleElement = {
|
||||
export type Handle = {
|
||||
id?: string | null;
|
||||
nodeId: string;
|
||||
x: number;
|
||||
y: number;
|
||||
position: Position;
|
||||
type: HandleType;
|
||||
width: number;
|
||||
height: number;
|
||||
position: Position;
|
||||
type?: HandleType;
|
||||
};
|
||||
|
||||
export type ConnectingHandle = {
|
||||
nodeId: string;
|
||||
type: HandleType;
|
||||
handleId?: string | null;
|
||||
};
|
||||
|
||||
export type ConnectionHandle = {
|
||||
id: string | null;
|
||||
type: HandleType;
|
||||
nodeId: string;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type HandleProps = {
|
||||
/** Type of the handle
|
||||
* @example HandleType.Source, HandleType.Target
|
||||
*/
|
||||
type: HandleType;
|
||||
/** Position of the handle
|
||||
* @example Position.TopLeft, Position.TopRight,
|
||||
* Position.BottomLeft, Position.BottomRight
|
||||
*/
|
||||
position: Position;
|
||||
/** Should you be able to connect to/from this handle */
|
||||
isConnectable?: boolean;
|
||||
/** Should you be able to connect from this handle */
|
||||
isConnectableStart?: boolean;
|
||||
/** Should you be able to connect to this handle */
|
||||
isConnectableEnd?: boolean;
|
||||
onConnect?: OnConnect;
|
||||
/** Callback if connection is valid
|
||||
* @remarks connection becomes an edge if isValidConnection returns true
|
||||
*/
|
||||
isValidConnection?: IsValidConnection;
|
||||
/** Id of the handle
|
||||
* @remarks optional if there is only one handle of this type
|
||||
*/
|
||||
id?: string;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './changes';
|
||||
export * from './general';
|
||||
export * from './nodes';
|
||||
export * from './edges';
|
||||
|
||||
@@ -1,73 +1,124 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { internalsSymbol } from '../constants';
|
||||
import type { XYPosition, Position, CoordinateExtent, HandleElement } from '.';
|
||||
import type { XYPosition, Position, CoordinateExtent, Handle } from '.';
|
||||
import { Optional } from '../utils/types';
|
||||
|
||||
// this is stuff that all nodes share independent of the framework
|
||||
export type NodeBase<T = any, U extends string | undefined = string | undefined> = {
|
||||
/**
|
||||
* Framework independent node data structure.
|
||||
*
|
||||
* @typeParam NodeData - type of the node data
|
||||
* @typeParam NodeType - type of the node
|
||||
*/
|
||||
export type NodeBase<
|
||||
NodeData extends Record<string, unknown> = Record<string, unknown>,
|
||||
NodeType extends string = string
|
||||
> = {
|
||||
/** Unique id of a node */
|
||||
id: string;
|
||||
/** Position of a node on the pane
|
||||
* @example { x: 0, y: 0 }
|
||||
*/
|
||||
position: XYPosition;
|
||||
data: T;
|
||||
type?: U;
|
||||
/** Arbitrary data passed to a node */
|
||||
data: NodeData;
|
||||
/** Type of node defined in nodeTypes */
|
||||
type?: NodeType;
|
||||
/** Only relevant for default, source, target nodeType. controls source position
|
||||
* @example 'right', 'left', 'top', 'bottom'
|
||||
*/
|
||||
sourcePosition?: Position;
|
||||
/** Only relevant for default, source, target nodeType. controls target position
|
||||
* @example 'right', 'left', 'top', 'bottom'
|
||||
*/
|
||||
targetPosition?: Position;
|
||||
hidden?: boolean;
|
||||
selected?: boolean;
|
||||
/** True, if node is being dragged */
|
||||
dragging?: boolean;
|
||||
draggable?: boolean;
|
||||
selectable?: boolean;
|
||||
connectable?: boolean;
|
||||
deletable?: boolean;
|
||||
dragHandle?: string;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
parentNode?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
initialWidth?: number;
|
||||
initialHeight?: number;
|
||||
/** Parent node id, used for creating sub-flows */
|
||||
parentId?: string;
|
||||
zIndex?: number;
|
||||
/** Boundary a node can be moved in
|
||||
* @example 'parent' or [[0, 0], [100, 100]]
|
||||
*/
|
||||
extent?: 'parent' | CoordinateExtent;
|
||||
expandParent?: boolean;
|
||||
positionAbsolute?: XYPosition;
|
||||
ariaLabel?: string;
|
||||
focusable?: boolean;
|
||||
/** Origin of the node relative to it's position
|
||||
* @example
|
||||
* [0.5, 0.5] // centers the node
|
||||
* [0, 0] // top left
|
||||
* [1, 1] // bottom right
|
||||
*/
|
||||
origin?: NodeOrigin;
|
||||
handles?: NodeHandle[];
|
||||
size?: {
|
||||
measured?: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
};
|
||||
|
||||
// only used internally
|
||||
[internalsSymbol]?: {
|
||||
z?: number;
|
||||
export type InternalNodeBase<NodeType extends NodeBase = NodeBase> = NodeType & {
|
||||
measured: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
internals: {
|
||||
positionAbsolute: XYPosition;
|
||||
z: number;
|
||||
/** Holds a reference to the original node object provided by the user.
|
||||
* Used as an optimization to avoid certain operations. */
|
||||
userNode: NodeType;
|
||||
handleBounds?: NodeHandleBounds;
|
||||
isParent?: boolean;
|
||||
bounds?: NodeBounds;
|
||||
};
|
||||
};
|
||||
|
||||
// props that get passed to a custom node
|
||||
export type NodeProps<T = any> = {
|
||||
id: NodeBase['id'];
|
||||
data: T;
|
||||
dragHandle: NodeBase['dragHandle'];
|
||||
type: NodeBase['type'];
|
||||
selected: NodeBase['selected'];
|
||||
isConnectable: NodeBase['connectable'];
|
||||
zIndex: NodeBase['zIndex'];
|
||||
xPos: number;
|
||||
yPos: number;
|
||||
dragging: boolean;
|
||||
targetPosition?: Position;
|
||||
sourcePosition?: Position;
|
||||
};
|
||||
/**
|
||||
* The node data structure that gets used for the nodes prop.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type NodeProps<NodeType extends NodeBase> = Pick<
|
||||
NodeType,
|
||||
| 'id'
|
||||
| 'data'
|
||||
| 'width'
|
||||
| 'height'
|
||||
| 'sourcePosition'
|
||||
| 'targetPosition'
|
||||
| 'selected'
|
||||
| 'dragHandle'
|
||||
| 'selectable'
|
||||
| 'deletable'
|
||||
| 'draggable'
|
||||
| 'parentId'
|
||||
> &
|
||||
Required<Pick<NodeType, 'type' | 'dragging' | 'zIndex'>> & {
|
||||
/** whether a node is connectable or not */
|
||||
isConnectable: boolean;
|
||||
/** position absolute x value */
|
||||
positionAbsoluteX: number;
|
||||
/** position absolute x value */
|
||||
positionAbsoluteY: number;
|
||||
};
|
||||
|
||||
export type NodeHandleBounds = {
|
||||
source: HandleElement[] | null;
|
||||
target: HandleElement[] | null;
|
||||
source: Handle[] | null;
|
||||
target: Handle[] | null;
|
||||
};
|
||||
|
||||
export type NodeDimensionUpdate = {
|
||||
export type InternalNodeUpdate = {
|
||||
id: string;
|
||||
nodeElement: HTMLDivElement;
|
||||
forceUpdate?: boolean;
|
||||
force?: boolean;
|
||||
};
|
||||
|
||||
export type NodeBounds = XYPosition & {
|
||||
@@ -78,13 +129,17 @@ export type NodeBounds = XYPosition & {
|
||||
export type NodeDragItem = {
|
||||
id: string;
|
||||
position: XYPosition;
|
||||
positionAbsolute: XYPosition;
|
||||
// distance from the mouse cursor to the node when start dragging
|
||||
distance: XYPosition;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
measured: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
internals: {
|
||||
positionAbsolute: XYPosition;
|
||||
};
|
||||
extent?: 'parent' | CoordinateExtent;
|
||||
parentNode?: string;
|
||||
parentId?: string;
|
||||
dragging?: boolean;
|
||||
origin?: NodeOrigin;
|
||||
expandParent?: boolean;
|
||||
@@ -92,8 +147,11 @@ export type NodeDragItem = {
|
||||
|
||||
export type NodeOrigin = [number, number];
|
||||
|
||||
export type OnNodeDrag = (event: MouseEvent, node: NodeBase, nodes: NodeBase[]) => void;
|
||||
|
||||
export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void;
|
||||
|
||||
export type NodeHandle = Optional<HandleElement, 'width' | 'height'>;
|
||||
export type NodeHandle = Omit<Optional<Handle, 'width' | 'height'>, 'nodeId'>;
|
||||
|
||||
export type Align = 'center' | 'start' | 'end';
|
||||
|
||||
export type NodeLookup<NodeType extends InternalNodeBase = InternalNodeBase> = Map<string, NodeType>;
|
||||
export type ParentLookup<NodeType extends InternalNodeBase = InternalNodeBase> = Map<string, Map<string, NodeType>>;
|
||||
|
||||
@@ -5,6 +5,13 @@ export enum Position {
|
||||
Bottom = 'bottom',
|
||||
}
|
||||
|
||||
export const oppositePosition = {
|
||||
[Position.Left]: Position.Right,
|
||||
[Position.Right]: Position.Left,
|
||||
[Position.Top]: Position.Bottom,
|
||||
[Position.Bottom]: Position.Top,
|
||||
};
|
||||
|
||||
export type XYPosition = {
|
||||
x: number;
|
||||
y: number;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { HandleConnection } from '../types';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function areConnectionMapsEqual(a?: Map<string, HandleConnection>, b?: Map<string, HandleConnection>) {
|
||||
if (!a && !b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!a || !b || a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!a.size && !b.size) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const key of a.keys()) {
|
||||
if (!b.has(key)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* We call the callback for all connections in a that are not in b
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function handleConnectionChange(
|
||||
a: Map<string, HandleConnection>,
|
||||
b: Map<string, HandleConnection>,
|
||||
cb?: (diff: HandleConnection[]) => void
|
||||
) {
|
||||
if (!cb) {
|
||||
return;
|
||||
}
|
||||
|
||||
const diff: HandleConnection[] = [];
|
||||
|
||||
a.forEach((connection, key) => {
|
||||
if (!b?.has(key)) {
|
||||
diff.push(connection);
|
||||
}
|
||||
});
|
||||
|
||||
if (diff.length) {
|
||||
cb(diff);
|
||||
}
|
||||
}
|
||||
|
||||
export function getConnectionStatus(isValid: boolean | null) {
|
||||
return isValid === null ? null : isValid ? 'valid' : 'invalid';
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Transform, XYPosition, SnapGrid, Dimensions, NodeOrigin, HandleElement, Position } from '../types';
|
||||
import type { Transform, XYPosition, SnapGrid, Dimensions, Position, Handle } from '../types';
|
||||
import { snapPosition, pointToRendererPoint } from './general';
|
||||
|
||||
export type GetPointerPositionParams = {
|
||||
@@ -13,7 +13,6 @@ export function getPointerPosition(
|
||||
): XYPosition & { xSnapped: number; ySnapped: number } {
|
||||
const { x, y } = getEventPosition(event);
|
||||
const pointerPos = pointToRendererPoint({ x, y }, transform);
|
||||
|
||||
const { x: xSnapped, y: ySnapped } = snapToGrid ? snapPosition(pointerPos, snapGrid) : pointerPos;
|
||||
|
||||
// we need the snapped position in order to be able to skip unnecessary drag events
|
||||
@@ -38,11 +37,9 @@ export function isInputDOMNode(event: KeyboardEvent): boolean {
|
||||
// using composed path for handling shadow dom
|
||||
const target = (event.composedPath?.()?.[0] || event.target) as HTMLElement;
|
||||
const isInput = inputTags.includes(target?.nodeName) || target?.hasAttribute('contenteditable');
|
||||
// we want to be able to do a multi selection event if we are in an input field
|
||||
const isModifierKey = event.ctrlKey || event.metaKey || event.shiftKey;
|
||||
|
||||
// when an input field is focused we don't want to trigger deletion or movement of nodes
|
||||
return (isInput && !isModifierKey) || !!target?.closest('.nokey');
|
||||
return isInput || !!target?.closest('.nokey');
|
||||
}
|
||||
|
||||
export const isMouseEvent = (event: MouseEvent | TouchEvent): event is MouseEvent => 'clientX' in event;
|
||||
@@ -58,34 +55,33 @@ export const getEventPosition = (event: MouseEvent | TouchEvent, bounds?: DOMRec
|
||||
};
|
||||
};
|
||||
|
||||
// The handle bounds are calculated relative to the node element.
|
||||
// We store them in the internals object of the node in order to avoid
|
||||
// unnecessary recalculations.
|
||||
export const getHandleBounds = (
|
||||
selector: string,
|
||||
type: 'source' | 'target',
|
||||
nodeElement: HTMLDivElement,
|
||||
nodeBounds: DOMRect,
|
||||
zoom: number,
|
||||
nodeOrigin: NodeOrigin = [0, 0]
|
||||
): HandleElement[] | null => {
|
||||
const handles = nodeElement.querySelectorAll(selector);
|
||||
nodeId: string
|
||||
): Handle[] | null => {
|
||||
const handles = nodeElement.querySelectorAll(`.${type}`);
|
||||
|
||||
if (!handles || !handles.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handlesArray = Array.from(handles) as HTMLDivElement[];
|
||||
const nodeBounds = nodeElement.getBoundingClientRect();
|
||||
const nodeOffset = {
|
||||
x: nodeBounds.width * nodeOrigin[0],
|
||||
y: nodeBounds.height * nodeOrigin[1],
|
||||
};
|
||||
|
||||
return handlesArray.map((handle): HandleElement => {
|
||||
return Array.from(handles).map((handle): Handle => {
|
||||
const handleBounds = handle.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
id: handle.getAttribute('data-handleid'),
|
||||
type,
|
||||
nodeId,
|
||||
position: handle.getAttribute('data-handlepos') as unknown as Position,
|
||||
x: (handleBounds.left - nodeBounds.left - nodeOffset.x) / zoom,
|
||||
y: (handleBounds.top - nodeBounds.top - nodeOffset.y) / zoom,
|
||||
...getDimensions(handle),
|
||||
x: (handleBounds.left - nodeBounds.left) / zoom,
|
||||
y: (handleBounds.top - nodeBounds.top) / zoom,
|
||||
...getDimensions(handle as HTMLDivElement),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -69,6 +69,29 @@ function getControlWithCurvature({ pos, x1, y1, x2, y2, c }: GetControlWithCurva
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a bezier path from source to target handle
|
||||
* @param params.sourceX - The x position of the source handle
|
||||
* @param params.sourceY - The y position of the source handle
|
||||
* @param params.sourcePosition - The position of the source handle (default: Position.Bottom)
|
||||
* @param params.targetX - The x position of the target handle
|
||||
* @param params.targetY - The y position of the target handle
|
||||
* @param params.targetPosition - The position of the target handle (default: Position.Top)
|
||||
* @param params.curvature - The curvature of the bezier edge
|
||||
* @returns A path string you can use in an SVG, the labelX and labelY position (center of path) and offsetX, offsetY between source handle and label
|
||||
* @example
|
||||
* const source = { x: 0, y: 20 };
|
||||
const target = { x: 150, y: 100 };
|
||||
|
||||
const [path, labelX, labelY, offsetX, offsetY] = getBezierPath({
|
||||
sourceX: source.x,
|
||||
sourceY: source.y,
|
||||
sourcePosition: Position.Right,
|
||||
targetX: target.x,
|
||||
targetY: target.y,
|
||||
targetPosition: Position.Left,
|
||||
});
|
||||
*/
|
||||
export function getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Connection, Transform, errorMessages, internalsSymbol, isEdgeBase } from '../..';
|
||||
import { EdgeBase, NodeBase } from '../../types';
|
||||
import { isNumeric, getOverlappingArea, boxToRect, nodeToBox, getBoundsOfBoxes, devWarn } from '../general';
|
||||
import { Connection, InternalNodeBase, Transform, errorMessages, isEdgeBase, EdgeBase } from '../..';
|
||||
import { getOverlappingArea, boxToRect, nodeToBox, getBoundsOfBoxes, devWarn } from '../general';
|
||||
|
||||
// this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB)
|
||||
export function getEdgeCenter({
|
||||
@@ -23,68 +22,34 @@ export function getEdgeCenter({
|
||||
return [centerX, centerY, xOffset, yOffset];
|
||||
}
|
||||
|
||||
const defaultEdgeTree = [{ level: 0, isMaxLevel: true, edges: [] }];
|
||||
|
||||
export type GroupedEdges<EdgeType extends EdgeBase> = {
|
||||
edges: EdgeType[];
|
||||
level: number;
|
||||
isMaxLevel: boolean;
|
||||
export type GetEdgeZIndexParams = {
|
||||
sourceNode: InternalNodeBase;
|
||||
targetNode: InternalNodeBase;
|
||||
selected?: boolean;
|
||||
zIndex?: number;
|
||||
elevateOnSelect?: boolean;
|
||||
};
|
||||
|
||||
export function groupEdgesByZLevel<EdgeType extends EdgeBase>(
|
||||
edges: EdgeType[],
|
||||
nodes: NodeBase[],
|
||||
elevateEdgesOnSelect = false
|
||||
): GroupedEdges<EdgeType>[] {
|
||||
let maxLevel = -1;
|
||||
|
||||
const levelLookup = edges.reduce<Record<string, EdgeType[]>>((tree, edge) => {
|
||||
const hasZIndex = isNumeric(edge.zIndex);
|
||||
let z = hasZIndex ? edge.zIndex! : 0;
|
||||
|
||||
if (elevateEdgesOnSelect) {
|
||||
const targetNode = nodes.find((n) => n.id === edge.target);
|
||||
const sourceNode = nodes.find((n) => n.id === edge.source);
|
||||
const edgeOrConnectedNodeSelected = edge.selected || targetNode?.selected || sourceNode?.selected;
|
||||
const selectedZIndex = Math.max(
|
||||
sourceNode?.[internalsSymbol]?.z || 0,
|
||||
targetNode?.[internalsSymbol]?.z || 0,
|
||||
1000
|
||||
);
|
||||
z = (hasZIndex ? edge.zIndex! : 0) + (edgeOrConnectedNodeSelected ? selectedZIndex : 0);
|
||||
}
|
||||
|
||||
if (tree[z]) {
|
||||
tree[z].push(edge);
|
||||
} else {
|
||||
tree[z] = [edge];
|
||||
}
|
||||
|
||||
maxLevel = z > maxLevel ? z : maxLevel;
|
||||
|
||||
return tree;
|
||||
}, {});
|
||||
|
||||
const edgeTree = Object.entries(levelLookup).map(([key, edges]) => {
|
||||
const level = +key;
|
||||
|
||||
return {
|
||||
edges,
|
||||
level,
|
||||
isMaxLevel: level === maxLevel,
|
||||
};
|
||||
});
|
||||
|
||||
if (edgeTree.length === 0) {
|
||||
return defaultEdgeTree;
|
||||
export function getElevatedEdgeZIndex({
|
||||
sourceNode,
|
||||
targetNode,
|
||||
selected = false,
|
||||
zIndex = 0,
|
||||
elevateOnSelect = false,
|
||||
}: GetEdgeZIndexParams): number {
|
||||
if (!elevateOnSelect) {
|
||||
return zIndex;
|
||||
}
|
||||
|
||||
return edgeTree;
|
||||
const edgeOrConnectedNodeSelected = selected || targetNode.selected || sourceNode.selected;
|
||||
const selectedZIndex = Math.max(sourceNode.internals.z || 0, targetNode.internals.z || 0, 1000);
|
||||
|
||||
return zIndex + (edgeOrConnectedNodeSelected ? selectedZIndex : 0);
|
||||
}
|
||||
|
||||
type IsEdgeVisibleParams = {
|
||||
sourceNode: NodeBase;
|
||||
targetNode: NodeBase;
|
||||
sourceNode: InternalNodeBase;
|
||||
targetNode: InternalNodeBase;
|
||||
width: number;
|
||||
height: number;
|
||||
transform: Transform;
|
||||
@@ -112,7 +77,7 @@ export function isEdgeVisible({ sourceNode, targetNode, width, height, transform
|
||||
}
|
||||
|
||||
const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection | EdgeBase): string =>
|
||||
`xyflow__edge-${source}${sourceHandle || ''}-${target}${targetHandle || ''}`;
|
||||
`xy-edge__${source}${sourceHandle || ''}-${target}${targetHandle || ''}`;
|
||||
|
||||
const connectionExists = (edge: EdgeBase, edges: EdgeBase[]) => {
|
||||
return edges.some(
|
||||
@@ -124,7 +89,15 @@ const connectionExists = (edge: EdgeBase, edges: EdgeBase[]) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const addEdgeBase = <EdgeType extends EdgeBase>(
|
||||
/**
|
||||
* This util is a convenience function to add a new Edge to an array of edges
|
||||
* @remarks It also performs some validation to make sure you don't add an invalid edge or duplicate an existing one.
|
||||
* @public
|
||||
* @param edgeParams - Either an Edge or a Connection you want to add
|
||||
* @param edges - The array of all current edges
|
||||
* @returns A new array of edges with the new edge added
|
||||
*/
|
||||
export const addEdge = <EdgeType extends EdgeBase>(
|
||||
edgeParams: EdgeType | Connection,
|
||||
edges: EdgeType[]
|
||||
): EdgeType[] => {
|
||||
@@ -148,18 +121,34 @@ export const addEdgeBase = <EdgeType extends EdgeBase>(
|
||||
return edges;
|
||||
}
|
||||
|
||||
if (edge.sourceHandle === null) {
|
||||
delete edge.sourceHandle;
|
||||
}
|
||||
|
||||
if (edge.targetHandle === null) {
|
||||
delete edge.targetHandle;
|
||||
}
|
||||
|
||||
return edges.concat(edge);
|
||||
};
|
||||
|
||||
export type UpdateEdgeOptions = {
|
||||
export type ReconnectEdgeOptions = {
|
||||
shouldReplaceId?: boolean;
|
||||
};
|
||||
|
||||
export const updateEdgeBase = <EdgeType extends EdgeBase>(
|
||||
/**
|
||||
* A handy utility to reconnect an existing edge with new properties
|
||||
* @param oldEdge - The edge you want to update
|
||||
* @param newConnection - The new connection you want to update the edge with
|
||||
* @param edges - The array of all current edges
|
||||
* @param options.shouldReplaceId - should the id of the old edge be replaced with the new connection id
|
||||
* @returns the updated edges array
|
||||
*/
|
||||
export const reconnectEdge = <EdgeType extends EdgeBase>(
|
||||
oldEdge: EdgeType,
|
||||
newConnection: Connection,
|
||||
edges: EdgeType[],
|
||||
options: UpdateEdgeOptions = { shouldReplaceId: true }
|
||||
options: ReconnectEdgeOptions = { shouldReplaceId: true }
|
||||
): EdgeType[] => {
|
||||
const { id: oldEdgeId, ...rest } = oldEdge;
|
||||
|
||||
|
||||
@@ -1,37 +1,47 @@
|
||||
import { EdgePosition } from '../../types/edges';
|
||||
import { ConnectionMode, OnError } from '../../types/general';
|
||||
import { NodeBase, NodeHandle, NodeHandleBounds } from '../../types/nodes';
|
||||
import { Position, Rect, XYPosition } from '../../types/utils';
|
||||
import { errorMessages, internalsSymbol } from '../../constants';
|
||||
import { HandleElement } from '../../types';
|
||||
import { InternalNodeBase, NodeHandle } from '../../types/nodes';
|
||||
import { Position, XYPosition } from '../../types/utils';
|
||||
import { errorMessages } from '../../constants';
|
||||
import { Handle } from '../../types';
|
||||
import { getNodeDimensions } from '../general';
|
||||
|
||||
export type GetEdgePositionParams = {
|
||||
id: string;
|
||||
sourceNode: NodeBase;
|
||||
sourceNode: InternalNodeBase;
|
||||
sourceHandle: string | null;
|
||||
targetNode: NodeBase;
|
||||
targetNode: InternalNodeBase;
|
||||
targetHandle: string | null;
|
||||
connectionMode: ConnectionMode;
|
||||
onError?: OnError;
|
||||
};
|
||||
|
||||
export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | null {
|
||||
const [sourceNodeRect, sourceHandleBounds, isSourceValid] = getHandleDataByNode(params.sourceNode);
|
||||
const [targetNodeRect, targetHandleBounds, isTargetValid] = getHandleDataByNode(params.targetNode);
|
||||
function isNodeInitialized(node: InternalNodeBase): boolean {
|
||||
return (
|
||||
node &&
|
||||
!!(node.internals.handleBounds || node.handles?.length) &&
|
||||
!!(node.measured.width || node.width || node.initialWidth)
|
||||
);
|
||||
}
|
||||
|
||||
if (!isSourceValid || !isTargetValid) {
|
||||
export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | null {
|
||||
const { sourceNode, targetNode } = params;
|
||||
|
||||
if (!isNodeInitialized(sourceNode) || !isNodeInitialized(targetNode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// when connection type is loose we can define all handles as sources and connect source -> source
|
||||
const targetNodeHandles =
|
||||
const sourceHandleBounds = sourceNode.internals.handleBounds || toHandleBounds(sourceNode.handles);
|
||||
const targetHandleBounds = targetNode.internals.handleBounds || toHandleBounds(targetNode.handles);
|
||||
|
||||
const sourceHandle = getHandle(sourceHandleBounds?.source ?? [], params.sourceHandle);
|
||||
const targetHandle = getHandle(
|
||||
// when connection type is loose we can define all handles as sources and connect source -> source
|
||||
params.connectionMode === ConnectionMode.Strict
|
||||
? targetHandleBounds!.target
|
||||
: (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []);
|
||||
const sourceHandle = getHandle(sourceHandleBounds!.source!, params.sourceHandle);
|
||||
const targetHandle = getHandle(targetNodeHandles!, params.targetHandle);
|
||||
const sourcePosition = sourceHandle?.position || Position.Bottom;
|
||||
const targetPosition = targetHandle?.position || Position.Top;
|
||||
? targetHandleBounds?.target ?? []
|
||||
: (targetHandleBounds?.target ?? []).concat(targetHandleBounds?.source ?? []),
|
||||
params.targetHandle
|
||||
);
|
||||
|
||||
if (!sourceHandle || !targetHandle) {
|
||||
params.onError?.(
|
||||
@@ -46,14 +56,16 @@ export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | n
|
||||
return null;
|
||||
}
|
||||
|
||||
const { x: sourceX, y: sourceY } = getHandlePosition(sourcePosition, sourceNodeRect, sourceHandle);
|
||||
const { x: targetX, y: targetY } = getHandlePosition(targetPosition, targetNodeRect, targetHandle);
|
||||
const sourcePosition = sourceHandle?.position || Position.Bottom;
|
||||
const targetPosition = targetHandle?.position || Position.Top;
|
||||
const source = getHandlePosition(sourceNode, sourceHandle, sourcePosition);
|
||||
const target = getHandlePosition(targetNode, targetHandle, targetPosition);
|
||||
|
||||
return {
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourceX: source.x,
|
||||
sourceY: source.y,
|
||||
targetX: target.x,
|
||||
targetY: target.y,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
};
|
||||
@@ -64,92 +76,59 @@ function toHandleBounds(handles?: NodeHandle[]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return handles.reduce<NodeHandleBounds>(
|
||||
(res, item) => {
|
||||
item.width = item.width || 1;
|
||||
item.height = item.height || 1;
|
||||
const source = [];
|
||||
const target = [];
|
||||
|
||||
if (item.type === 'source') {
|
||||
res.source?.push(item as HandleElement);
|
||||
}
|
||||
for (const handle of handles) {
|
||||
handle.width = handle.width ?? 1;
|
||||
handle.height = handle.height ?? 1;
|
||||
|
||||
if (item.type === 'target') {
|
||||
res.target?.push(item as HandleElement);
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
{
|
||||
source: [],
|
||||
target: [],
|
||||
if (handle.type === 'source') {
|
||||
source.push(handle as Handle);
|
||||
} else if (handle.type === 'target') {
|
||||
target.push(handle as Handle);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
source,
|
||||
target,
|
||||
};
|
||||
}
|
||||
|
||||
function getHandleDataByNode(node?: NodeBase): [Rect, NodeHandleBounds | null, boolean] {
|
||||
const handleBounds = node?.[internalsSymbol]?.handleBounds || toHandleBounds(node?.handles) || null;
|
||||
const nodeWidth = node?.width || node?.size?.width;
|
||||
const nodeHeight = node?.height || node?.size?.height;
|
||||
export function getHandlePosition(
|
||||
node: InternalNodeBase,
|
||||
handle: Handle | null,
|
||||
fallbackPosition: Position = Position.Left,
|
||||
center = false
|
||||
): XYPosition {
|
||||
const x = (handle?.x ?? 0) + node.internals.positionAbsolute.x;
|
||||
const y = (handle?.y ?? 0) + node.internals.positionAbsolute.y;
|
||||
const { width, height } = handle ?? getNodeDimensions(node);
|
||||
|
||||
const isValid =
|
||||
handleBounds &&
|
||||
nodeWidth &&
|
||||
nodeHeight &&
|
||||
typeof node?.positionAbsolute?.x !== 'undefined' &&
|
||||
typeof node?.positionAbsolute?.y !== 'undefined';
|
||||
if (center) {
|
||||
return { x: x + width / 2, y: y + height / 2 };
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
x: node?.positionAbsolute?.x || 0,
|
||||
y: node?.positionAbsolute?.y || 0,
|
||||
width: nodeWidth || 0,
|
||||
height: nodeHeight || 0,
|
||||
},
|
||||
handleBounds,
|
||||
!!isValid,
|
||||
];
|
||||
}
|
||||
|
||||
function getHandlePosition(position: Position, nodeRect: Rect, handle: HandleElement | null = null): XYPosition {
|
||||
const x = (handle?.x || 0) + nodeRect.x;
|
||||
const y = (handle?.y || 0) + nodeRect.y;
|
||||
const width = handle?.width || nodeRect.width;
|
||||
const height = handle?.height || nodeRect.height;
|
||||
const position = handle?.position ?? fallbackPosition;
|
||||
|
||||
switch (position) {
|
||||
case Position.Top:
|
||||
return {
|
||||
x: x + width / 2,
|
||||
y,
|
||||
};
|
||||
return { x: x + width / 2, y };
|
||||
case Position.Right:
|
||||
return {
|
||||
x: x + width,
|
||||
y: y + height / 2,
|
||||
};
|
||||
return { x: x + width, y: y + height / 2 };
|
||||
case Position.Bottom:
|
||||
return {
|
||||
x: x + width / 2,
|
||||
y: y + height,
|
||||
};
|
||||
return { x: x + width / 2, y: y + height };
|
||||
case Position.Left:
|
||||
return {
|
||||
x,
|
||||
y: y + height / 2,
|
||||
};
|
||||
return { x, y: y + height / 2 };
|
||||
}
|
||||
}
|
||||
|
||||
function getHandle(bounds: HandleElement[], handleId?: string | null): HandleElement | null {
|
||||
function getHandle(bounds: Handle[], handleId?: string | null): Handle | null {
|
||||
if (!bounds) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (bounds.length === 1 || !handleId) {
|
||||
return bounds[0];
|
||||
} else if (handleId) {
|
||||
return bounds.find((d) => d.id === handleId) || null;
|
||||
}
|
||||
|
||||
return null;
|
||||
// if no handleId is given, we use the first handle, otherwise we check for the id
|
||||
return (!handleId ? bounds[0] : bounds.find((d) => d.id === handleId)) || null;
|
||||
}
|
||||
|
||||
@@ -81,8 +81,8 @@ function getPoints({
|
||||
|
||||
// opposite handle positions, default case
|
||||
if (sourceDir[dirAccessor] * targetDir[dirAccessor] === -1) {
|
||||
centerX = center.x || defaultCenterX;
|
||||
centerY = center.y || defaultCenterY;
|
||||
centerX = center.x ?? defaultCenterX;
|
||||
centerY = center.y ?? defaultCenterY;
|
||||
// --->
|
||||
// |
|
||||
// >---
|
||||
@@ -190,6 +190,28 @@ function getBend(a: XYPosition, b: XYPosition, c: XYPosition, size: number): str
|
||||
return `L ${x},${y + bendSize * yDir}Q ${x},${y} ${x + bendSize * xDir},${y}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a smooth step path from source to target handle
|
||||
* @param params.sourceX - The x position of the source handle
|
||||
* @param params.sourceY - The y position of the source handle
|
||||
* @param params.sourcePosition - The position of the source handle (default: Position.Bottom)
|
||||
* @param params.targetX - The x position of the target handle
|
||||
* @param params.targetY - The y position of the target handle
|
||||
* @param params.targetPosition - The position of the target handle (default: Position.Top)
|
||||
* @returns A path string you can use in an SVG, the labelX and labelY position (center of path) and offsetX, offsetY between source handle and label
|
||||
* @example
|
||||
* const source = { x: 0, y: 20 };
|
||||
const target = { x: 150, y: 100 };
|
||||
|
||||
const [path, labelX, labelY, offsetX, offsetY] = getSmoothStepPath({
|
||||
sourceX: source.x,
|
||||
sourceY: source.y,
|
||||
sourcePosition: Position.Right,
|
||||
targetX: target.x,
|
||||
targetY: target.y,
|
||||
targetPosition: Position.Left,
|
||||
});
|
||||
*/
|
||||
export function getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
|
||||
@@ -7,6 +7,26 @@ export type GetStraightPathParams = {
|
||||
targetY: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a straight path from source to target handle
|
||||
* @param params.sourceX - The x position of the source handle
|
||||
* @param params.sourceY - The y position of the source handle
|
||||
* @param params.targetX - The x position of the target handle
|
||||
* @param params.targetY - The y position of the target handle
|
||||
* @returns A path string you can use in an SVG, the labelX and labelY position (center of path) and offsetX, offsetY between source handle and label
|
||||
* @example
|
||||
* const source = { x: 0, y: 20 };
|
||||
const target = { x: 150, y: 100 };
|
||||
|
||||
const [path, labelX, labelY, offsetX, offsetY] = getStraightPath({
|
||||
sourceX: source.x,
|
||||
sourceY: source.y,
|
||||
sourcePosition: Position.Right,
|
||||
targetX: target.x,
|
||||
targetY: target.y,
|
||||
targetPosition: Position.Left,
|
||||
});
|
||||
*/
|
||||
export function getStraightPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
|
||||
@@ -8,8 +8,11 @@ import type {
|
||||
NodeOrigin,
|
||||
SnapGrid,
|
||||
Transform,
|
||||
InternalNodeBase,
|
||||
NodeLookup,
|
||||
} from '../types';
|
||||
import { getNodePositionWithOrigin } from './graph';
|
||||
import { type Viewport } from '../types';
|
||||
import { getNodePositionWithOrigin, isInternalNodeBase } from './graph';
|
||||
|
||||
export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max);
|
||||
|
||||
@@ -18,21 +21,32 @@ export const clampPosition = (position: XYPosition = { x: 0, y: 0 }, extent: Coo
|
||||
y: clamp(position.y, extent[0][1], extent[1][1]),
|
||||
});
|
||||
|
||||
// returns a number between 0 and 1 that represents the velocity of the movement
|
||||
// when the mouse is close to the edge of the canvas
|
||||
/**
|
||||
* Calculates the velocity of panning when the mouse is close to the edge of the canvas
|
||||
* @internal
|
||||
* @param value - One dimensional poition of the mouse (x or y)
|
||||
* @param min - Minimal position on canvas before panning starts
|
||||
* @param max - Maximal position on canvas before panning starts
|
||||
* @returns - A number between 0 and 1 that represents the velocity of panning
|
||||
*/
|
||||
const calcAutoPanVelocity = (value: number, min: number, max: number): number => {
|
||||
if (value < min) {
|
||||
return clamp(Math.abs(value - min), 1, 50) / 50;
|
||||
return clamp(Math.abs(value - min), 1, min) / min;
|
||||
} else if (value > max) {
|
||||
return -clamp(Math.abs(value - max), 1, 50) / 50;
|
||||
return -clamp(Math.abs(value - max), 1, min) / min;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const calcAutoPan = (pos: XYPosition, bounds: Dimensions): number[] => {
|
||||
const xMovement = calcAutoPanVelocity(pos.x, 35, bounds.width - 35) * 20;
|
||||
const yMovement = calcAutoPanVelocity(pos.y, 35, bounds.height - 35) * 20;
|
||||
export const calcAutoPan = (
|
||||
pos: XYPosition,
|
||||
bounds: Dimensions,
|
||||
speed: number = 15,
|
||||
distance: number = 40
|
||||
): number[] => {
|
||||
const xMovement = calcAutoPanVelocity(pos.x, distance, bounds.width - distance) * speed;
|
||||
const yMovement = calcAutoPanVelocity(pos.y, distance, bounds.height - distance) * speed;
|
||||
|
||||
return [xMovement, yMovement];
|
||||
};
|
||||
@@ -58,23 +72,29 @@ export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
|
||||
height: y2 - y,
|
||||
});
|
||||
|
||||
export const nodeToRect = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rect => {
|
||||
const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
|
||||
export const nodeToRect = (node: InternalNodeBase | NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rect => {
|
||||
const { x, y } = isInternalNodeBase(node)
|
||||
? node.internals.positionAbsolute
|
||||
: getNodePositionWithOrigin(node, nodeOrigin);
|
||||
|
||||
return {
|
||||
...positionAbsolute,
|
||||
width: node.width || 0,
|
||||
height: node.height || 0,
|
||||
x,
|
||||
y,
|
||||
width: node.measured?.width ?? node.width ?? node.initialWidth ?? 0,
|
||||
height: node.measured?.height ?? node.height ?? node.initialHeight ?? 0,
|
||||
};
|
||||
};
|
||||
|
||||
export const nodeToBox = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Box => {
|
||||
const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
|
||||
export const nodeToBox = (node: InternalNodeBase | NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Box => {
|
||||
const { x, y } = isInternalNodeBase(node)
|
||||
? node.internals.positionAbsolute
|
||||
: getNodePositionWithOrigin(node, nodeOrigin);
|
||||
|
||||
return {
|
||||
...positionAbsolute,
|
||||
x2: positionAbsolute.x + (node.width || 0),
|
||||
y2: positionAbsolute.y + (node.height || 0),
|
||||
x,
|
||||
y,
|
||||
x2: x + (node.measured?.width ?? node.width ?? node.initialWidth ?? 0),
|
||||
y2: y + (node.measured?.height ?? node.height ?? node.initialHeight ?? 0),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -103,35 +123,12 @@ export const devWarn = (id: string, message: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getPositionWithOrigin = ({
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
origin = [0, 0],
|
||||
}: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
origin?: NodeOrigin;
|
||||
}): XYPosition => {
|
||||
if (!width || !height || origin[0] < 0 || origin[1] < 0 || origin[0] > 1 || origin[1] > 1) {
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
return {
|
||||
x: x - width * origin[0],
|
||||
y: y - height * origin[1],
|
||||
};
|
||||
};
|
||||
|
||||
export function snapPosition(position: XYPosition, snapGrid: SnapGrid = [1, 1]): XYPosition {
|
||||
export const snapPosition = (position: XYPosition, snapGrid: SnapGrid = [1, 1]): XYPosition => {
|
||||
return {
|
||||
x: snapGrid[0] * Math.round(position.x / snapGrid[0]),
|
||||
y: snapGrid[1] * Math.round(position.y / snapGrid[1]),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const pointToRendererPoint = (
|
||||
{ x, y }: XYPosition,
|
||||
@@ -154,14 +151,30 @@ export const rendererPointToPoint = ({ x, y }: XYPosition, [tx, ty, tScale]: Tra
|
||||
};
|
||||
};
|
||||
|
||||
export const getTransformForBounds = (
|
||||
/**
|
||||
* Returns a viewport that encloses the given bounds with optional padding.
|
||||
* @public
|
||||
* @remarks You can determine bounds of nodes with {@link getNodesBounds} and {@link getBoundsOfRects}
|
||||
* @param bounds - Bounds to fit inside viewport
|
||||
* @param width - Width of the viewport
|
||||
* @param height - Height of the viewport
|
||||
* @param minZoom - Minimum zoom level of the resulting viewport
|
||||
* @param maxZoom - Maximum zoom level of the resulting viewport
|
||||
* @param padding - Optional padding around the bounds
|
||||
* @returns A transforned {@link Viewport} that encloses the given bounds which you can pass to e.g. {@link setViewport}
|
||||
* @example
|
||||
* const { x, y, zoom } = getViewportForBounds(
|
||||
{ x: 0, y: 0, width: 100, height: 100},
|
||||
1200, 800, 0.5, 2);
|
||||
*/
|
||||
export const getViewportForBounds = (
|
||||
bounds: Rect,
|
||||
width: number,
|
||||
height: number,
|
||||
minZoom: number,
|
||||
maxZoom: number,
|
||||
padding: number
|
||||
): Transform => {
|
||||
): Viewport => {
|
||||
const xZoom = width / (bounds.width * (1 + padding));
|
||||
const yZoom = height / (bounds.height * (1 + padding));
|
||||
const zoom = Math.min(xZoom, yZoom);
|
||||
@@ -171,7 +184,65 @@ export const getTransformForBounds = (
|
||||
const x = width / 2 - boundsCenterX * clampedZoom;
|
||||
const y = height / 2 - boundsCenterY * clampedZoom;
|
||||
|
||||
return [x, y, clampedZoom];
|
||||
return { x, y, zoom: clampedZoom };
|
||||
};
|
||||
|
||||
export const isMacOs = () => typeof navigator !== 'undefined' && navigator?.userAgent?.indexOf('Mac') >= 0;
|
||||
|
||||
export function isCoordinateExtent(extent?: CoordinateExtent | 'parent'): extent is CoordinateExtent {
|
||||
return extent !== undefined && extent !== 'parent';
|
||||
}
|
||||
|
||||
export function getNodeDimensions(node: {
|
||||
measured?: { width?: number; height?: number };
|
||||
width?: number;
|
||||
height?: number;
|
||||
initialWidth?: number;
|
||||
initialHeight?: number;
|
||||
}): { width: number; height: number } {
|
||||
return {
|
||||
width: node.measured?.width ?? node.width ?? node.initialWidth ?? 0,
|
||||
height: node.measured?.height ?? node.height ?? node.initialHeight ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function nodeHasDimensions<NodeType extends NodeBase = NodeBase>(node: NodeType): boolean {
|
||||
return (
|
||||
(node.measured?.width ?? node.width ?? node.initialWidth) !== undefined &&
|
||||
(node.measured?.height ?? node.height ?? node.initialHeight) !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert child position to aboslute position
|
||||
*
|
||||
* @internal
|
||||
* @param position
|
||||
* @param parentId
|
||||
* @param nodeLookup
|
||||
* @param nodeOrigin
|
||||
* @returns an internal node with an absolute position
|
||||
*/
|
||||
export function evaluateAbsolutePosition(
|
||||
position: XYPosition,
|
||||
dimensions: { width?: number; height?: number } = { width: 0, height: 0 },
|
||||
parentId: string,
|
||||
nodeLookup: NodeLookup,
|
||||
nodeOrigin: NodeOrigin
|
||||
): XYPosition {
|
||||
let nextParentId: string | undefined = parentId;
|
||||
const positionAbsolute = { ...position };
|
||||
|
||||
while (nextParentId) {
|
||||
const parent = nodeLookup.get(nextParentId);
|
||||
nextParentId = parent?.parentId;
|
||||
|
||||
if (parent) {
|
||||
const origin = parent.origin || nodeOrigin;
|
||||
positionAbsolute.x += parent.internals.positionAbsolute.x - (dimensions.width ?? 0) * origin[0];
|
||||
positionAbsolute.y += parent.internals.positionAbsolute.y - (dimensions.height ?? 0) * origin[1];
|
||||
}
|
||||
}
|
||||
|
||||
return positionAbsolute;
|
||||
}
|
||||
|
||||
+255
-150
@@ -4,14 +4,14 @@ import {
|
||||
clampPosition,
|
||||
getBoundsOfBoxes,
|
||||
getOverlappingArea,
|
||||
isNumeric,
|
||||
rectToBox,
|
||||
nodeToRect,
|
||||
pointToRendererPoint,
|
||||
getTransformForBounds,
|
||||
getViewportForBounds,
|
||||
isCoordinateExtent,
|
||||
getNodeDimensions,
|
||||
nodeToBox,
|
||||
} from './general';
|
||||
import {
|
||||
type Connection,
|
||||
type Transform,
|
||||
type XYPosition,
|
||||
type Rect,
|
||||
@@ -20,21 +20,48 @@ import {
|
||||
type EdgeBase,
|
||||
type FitViewParamsBase,
|
||||
type FitViewOptionsBase,
|
||||
NodeDragItem,
|
||||
CoordinateExtent,
|
||||
OnError,
|
||||
OnBeforeDeleteBase,
|
||||
NodeLookup,
|
||||
InternalNodeBase,
|
||||
NodeDragItem,
|
||||
} from '../types';
|
||||
import { errorMessages } from '../constants';
|
||||
|
||||
export const isEdgeBase = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
|
||||
element: NodeType | Connection | EdgeType
|
||||
): element is EdgeType => 'id' in element && 'source' in element && 'target' in element;
|
||||
/**
|
||||
* Test whether an object is useable as an Edge
|
||||
* @public
|
||||
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Edge if it returns true
|
||||
* @param element - The element to test
|
||||
* @returns A boolean indicating whether the element is an Edge
|
||||
*/
|
||||
export const isEdgeBase = <EdgeType extends EdgeBase = EdgeBase>(element: any): element is EdgeType =>
|
||||
'id' in element && 'source' in element && 'target' in element;
|
||||
|
||||
export const isNodeBase = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
|
||||
element: NodeType | Connection | EdgeType
|
||||
): element is NodeType => 'id' in element && !('source' in element) && !('target' in element);
|
||||
/**
|
||||
* Test whether an object is useable as a Node
|
||||
* @public
|
||||
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Node if it returns true
|
||||
* @param element - The element to test
|
||||
* @returns A boolean indicating whether the element is an Node
|
||||
*/
|
||||
export const isNodeBase = <NodeType extends NodeBase = NodeBase>(element: any): element is NodeType =>
|
||||
'id' in element && 'position' in element && !('source' in element) && !('target' in element);
|
||||
|
||||
export const getOutgoersBase = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
|
||||
export const isInternalNodeBase = <NodeType extends InternalNodeBase = InternalNodeBase>(
|
||||
element: any
|
||||
): element is NodeType => 'id' in element && 'internals' in element && !('source' in element) && !('target' in element);
|
||||
|
||||
/**
|
||||
* Pass in a node, and get connected nodes where edge.source === node.id
|
||||
* @public
|
||||
* @param node - The node to get the connected nodes from
|
||||
* @param nodes - The array of all nodes
|
||||
* @param edges - The array of all edges
|
||||
* @returns An array of nodes that are connected over eges where the source is the given node
|
||||
*/
|
||||
export const getOutgoers = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
|
||||
node: NodeType | { id: string },
|
||||
nodes: NodeType[],
|
||||
edges: EdgeType[]
|
||||
@@ -53,7 +80,15 @@ export const getOutgoersBase = <NodeType extends NodeBase = NodeBase, EdgeType e
|
||||
return nodes.filter((n) => outgoerIds.has(n.id));
|
||||
};
|
||||
|
||||
export const getIncomersBase = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
|
||||
/**
|
||||
* Pass in a node, and get connected nodes where edge.target === node.id
|
||||
* @public
|
||||
* @param node - The node to get the connected nodes from
|
||||
* @param nodes - The array of all nodes
|
||||
* @param edges - The array of all edges
|
||||
* @returns An array of nodes that are connected over eges where the target is the given node
|
||||
*/
|
||||
export const getIncomers = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
|
||||
node: NodeType | { id: string },
|
||||
nodes: NodeType[],
|
||||
edges: EdgeType[]
|
||||
@@ -71,57 +106,39 @@ export const getIncomersBase = <NodeType extends NodeBase = NodeBase, EdgeType e
|
||||
return nodes.filter((n) => incomersIds.has(n.id));
|
||||
};
|
||||
|
||||
export const getNodePositionWithOrigin = (
|
||||
node: NodeBase | undefined,
|
||||
nodeOrigin: NodeOrigin = [0, 0]
|
||||
): XYPosition & { positionAbsolute: XYPosition } => {
|
||||
if (!node) {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
positionAbsolute: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
export const getNodePositionWithOrigin = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): XYPosition => {
|
||||
const { width, height } = getNodeDimensions(node);
|
||||
const origin = node.origin ?? nodeOrigin;
|
||||
const offsetX = width * origin[0];
|
||||
const offsetY = height * origin[1];
|
||||
|
||||
const offsetX = (node.width ?? 0) * nodeOrigin[0];
|
||||
const offsetY = (node.height ?? 0) * nodeOrigin[1];
|
||||
|
||||
const position: XYPosition = {
|
||||
return {
|
||||
x: node.position.x - offsetX,
|
||||
y: node.position.y - offsetY,
|
||||
};
|
||||
|
||||
return {
|
||||
...position,
|
||||
positionAbsolute: node.positionAbsolute
|
||||
? {
|
||||
x: node.positionAbsolute.x - offsetX,
|
||||
y: node.positionAbsolute.y - offsetY,
|
||||
}
|
||||
: position,
|
||||
};
|
||||
};
|
||||
|
||||
export const getRectOfNodes = (nodes: NodeBase[], nodeOrigin: NodeOrigin = [0, 0]): Rect => {
|
||||
export type GetNodesBoundsParams = {
|
||||
nodeOrigin?: NodeOrigin;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines a bounding box that contains all given nodes in an array
|
||||
* @public
|
||||
* @remarks Useful when combined with {@link getViewportForBounds} to calculate the correct transform to fit the given nodes in a viewport.
|
||||
* @param nodes - Nodes to calculate the bounds for
|
||||
* @param params.nodeOrigin - Origin of the nodes: [0, 0] - top left, [0.5, 0.5] - center
|
||||
* @returns Bounding box enclosing all nodes
|
||||
*/
|
||||
export const getNodesBounds = (nodes: NodeBase[], params: GetNodesBoundsParams = { nodeOrigin: [0, 0] }): Rect => {
|
||||
if (nodes.length === 0) {
|
||||
return { x: 0, y: 0, width: 0, height: 0 };
|
||||
}
|
||||
|
||||
const box = nodes.reduce(
|
||||
(currBox, node) => {
|
||||
const { x, y } = getNodePositionWithOrigin(node, node.origin || nodeOrigin).positionAbsolute;
|
||||
return getBoundsOfBoxes(
|
||||
currBox,
|
||||
rectToBox({
|
||||
x,
|
||||
y,
|
||||
width: node.width || 0,
|
||||
height: node.height || 0,
|
||||
})
|
||||
);
|
||||
const nodeBox = nodeToBox(node, params.nodeOrigin);
|
||||
return getBoundsOfBoxes(currBox, nodeBox);
|
||||
},
|
||||
{ x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity }
|
||||
);
|
||||
@@ -129,46 +146,82 @@ export const getRectOfNodes = (nodes: NodeBase[], nodeOrigin: NodeOrigin = [0, 0
|
||||
return boxToRect(box);
|
||||
};
|
||||
|
||||
export const getNodesInside = <NodeType extends NodeBase>(
|
||||
nodes: NodeType[],
|
||||
export type GetInternalNodesBoundsParams<NodeType> = {
|
||||
useRelativePosition?: boolean;
|
||||
filter?: (node: NodeType) => boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines a bounding box that contains all given nodes in an array
|
||||
* @internal
|
||||
*/
|
||||
export const getInternalNodesBounds = <NodeType extends InternalNodeBase | NodeDragItem>(
|
||||
nodeLookup: Map<string, NodeType>,
|
||||
params: GetInternalNodesBoundsParams<NodeType> = {}
|
||||
): Rect => {
|
||||
if (nodeLookup.size === 0) {
|
||||
return { x: 0, y: 0, width: 0, height: 0 };
|
||||
}
|
||||
|
||||
let box = { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity };
|
||||
|
||||
nodeLookup.forEach((node) => {
|
||||
if (params.filter === undefined || params.filter(node)) {
|
||||
const nodeBox = nodeToBox(node as InternalNodeBase);
|
||||
box = getBoundsOfBoxes(box, nodeBox);
|
||||
}
|
||||
});
|
||||
|
||||
return boxToRect(box);
|
||||
};
|
||||
|
||||
export const getNodesInside = <NodeType extends NodeBase = NodeBase>(
|
||||
nodes: Map<string, InternalNodeBase<NodeType>>,
|
||||
rect: Rect,
|
||||
[tx, ty, tScale]: Transform = [0, 0, 1],
|
||||
partially = false,
|
||||
// set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute
|
||||
excludeNonSelectableNodes = false,
|
||||
nodeOrigin: NodeOrigin = [0, 0]
|
||||
): NodeType[] => {
|
||||
excludeNonSelectableNodes = false
|
||||
): InternalNodeBase<NodeType>[] => {
|
||||
const paneRect = {
|
||||
...pointToRendererPoint(rect, [tx, ty, tScale]),
|
||||
width: rect.width / tScale,
|
||||
height: rect.height / tScale,
|
||||
};
|
||||
|
||||
const visibleNodes = nodes.reduce<NodeType[]>((res, node) => {
|
||||
const { width, height, selectable = true, hidden = false } = node;
|
||||
const visibleNodes: InternalNodeBase<NodeType>[] = [];
|
||||
|
||||
for (const [, node] of nodes) {
|
||||
const { measured, selectable = true, hidden = false } = node;
|
||||
const width = measured.width ?? node.width ?? node.initialWidth ?? null;
|
||||
const height = measured.height ?? node.height ?? node.initialHeight ?? null;
|
||||
|
||||
if ((excludeNonSelectableNodes && !selectable) || hidden) {
|
||||
return res;
|
||||
continue;
|
||||
}
|
||||
|
||||
const overlappingArea = getOverlappingArea(paneRect, nodeToRect(node, nodeOrigin));
|
||||
const notInitialized = width === undefined || height === undefined || width === null || height === null;
|
||||
const overlappingArea = getOverlappingArea(paneRect, nodeToRect(node));
|
||||
const notInitialized = width === null || height === null;
|
||||
|
||||
const partiallyVisible = partially && overlappingArea > 0;
|
||||
const area = (width || 0) * (height || 0);
|
||||
const area = (width ?? 0) * (height ?? 0);
|
||||
const isVisible = notInitialized || partiallyVisible || overlappingArea >= area;
|
||||
|
||||
if (isVisible || node.dragging) {
|
||||
res.push(node);
|
||||
visibleNodes.push(node);
|
||||
}
|
||||
|
||||
return res;
|
||||
}, []);
|
||||
}
|
||||
|
||||
return visibleNodes;
|
||||
};
|
||||
|
||||
export const getConnectedEdgesBase = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
|
||||
/**
|
||||
* Get all connecting edges for a given set of nodes
|
||||
* @param nodes - Nodes you want to get the connected edges for
|
||||
* @param edges - All edges
|
||||
* @returns Array of edges that connect any of the given nodes with each other
|
||||
*/
|
||||
export const getConnectedEdges = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
|
||||
nodes: NodeType[],
|
||||
edges: EdgeType[]
|
||||
): EdgeType[] => {
|
||||
@@ -181,23 +234,24 @@ export const getConnectedEdgesBase = <NodeType extends NodeBase = NodeBase, Edge
|
||||
};
|
||||
|
||||
export function fitView<Params extends FitViewParamsBase<NodeBase>, Options extends FitViewOptionsBase<NodeBase>>(
|
||||
{ nodes, width, height, panZoom, minZoom, maxZoom, nodeOrigin = [0, 0] }: Params,
|
||||
{ nodeLookup, width, height, panZoom, minZoom, maxZoom }: Params,
|
||||
options?: Options
|
||||
) {
|
||||
const filteredNodes = nodes.filter((n) => {
|
||||
const isVisible = n.width && n.height && (options?.includeHiddenNodes || !n.hidden);
|
||||
const filteredNodes: Map<string, InternalNodeBase> = new Map();
|
||||
const optionNodeIds = options?.nodes ? new Set(options.nodes.map((node) => node.id)) : null;
|
||||
|
||||
if (options?.nodes?.length) {
|
||||
return isVisible && options?.nodes.some((optionNode) => optionNode.id === n.id);
|
||||
nodeLookup.forEach((n) => {
|
||||
const isVisible = n.measured.width && n.measured.height && (options?.includeHiddenNodes || !n.hidden);
|
||||
|
||||
if (isVisible && (!optionNodeIds || optionNodeIds.has(n.id))) {
|
||||
filteredNodes.set(n.id, n);
|
||||
}
|
||||
|
||||
return isVisible;
|
||||
});
|
||||
|
||||
if (filteredNodes.length > 0) {
|
||||
const bounds = getRectOfNodes(filteredNodes, nodeOrigin);
|
||||
if (filteredNodes.size > 0) {
|
||||
const bounds = getInternalNodesBounds(filteredNodes);
|
||||
|
||||
const [x, y, zoom] = getTransformForBounds(
|
||||
const viewport = getViewportForBounds(
|
||||
bounds,
|
||||
width,
|
||||
height,
|
||||
@@ -206,7 +260,7 @@ export function fitView<Params extends FitViewParamsBase<NodeBase>, Options exte
|
||||
options?.padding ?? 0.1
|
||||
);
|
||||
|
||||
panZoom.setViewport({ x, y, zoom }, { duration: options?.duration });
|
||||
panZoom.setViewport(viewport, { duration: options?.duration });
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -214,108 +268,159 @@ export function fitView<Params extends FitViewParamsBase<NodeBase>, Options exte
|
||||
return false;
|
||||
}
|
||||
|
||||
function clampNodeExtent(node: NodeDragItem | NodeBase, extent?: CoordinateExtent | 'parent') {
|
||||
/**
|
||||
* This function clamps the passed extend by the node's width and height.
|
||||
* This is needed to prevent the node from being dragged outside of its extent.
|
||||
*
|
||||
* @param node
|
||||
* @param extent
|
||||
* @returns
|
||||
*/
|
||||
function clampNodeExtent<NodeType extends NodeBase>(
|
||||
node: NodeType,
|
||||
extent?: CoordinateExtent | 'parent'
|
||||
): CoordinateExtent | 'parent' | undefined {
|
||||
if (!extent || extent === 'parent') {
|
||||
return extent;
|
||||
}
|
||||
return [extent[0], [extent[1][0] - (node.width || 0), extent[1][1] - (node.height || 0)]];
|
||||
return [extent[0], [extent[1][0] - (node.measured?.width ?? 0), extent[1][1] - (node.measured?.height ?? 0)]];
|
||||
}
|
||||
|
||||
export function calcNextPosition<NodeType extends NodeBase>(
|
||||
node: NodeDragItem | NodeType,
|
||||
nextPosition: XYPosition,
|
||||
nodes: NodeType[],
|
||||
nodeExtent?: CoordinateExtent,
|
||||
nodeOrigin: NodeOrigin = [0, 0],
|
||||
onError?: OnError
|
||||
): { position: XYPosition; positionAbsolute: XYPosition } {
|
||||
const clampedNodeExtent = clampNodeExtent(node, node.extent || nodeExtent);
|
||||
let currentExtent = clampedNodeExtent;
|
||||
let parentNode: NodeType | null = null;
|
||||
let parentPos = { x: 0, y: 0 };
|
||||
/**
|
||||
* This function calculates the next position of a node, taking into account the node's extent, parent node, and origin.
|
||||
*
|
||||
* @internal
|
||||
* @returns position, positionAbsolute
|
||||
*/
|
||||
export function calculateNodePosition<NodeType extends NodeBase>({
|
||||
nodeId,
|
||||
nextPosition,
|
||||
nodeLookup,
|
||||
nodeOrigin = [0, 0],
|
||||
nodeExtent,
|
||||
onError,
|
||||
}: {
|
||||
nodeId: string;
|
||||
nextPosition: XYPosition;
|
||||
nodeLookup: NodeLookup<InternalNodeBase<NodeType>>;
|
||||
nodeOrigin?: NodeOrigin;
|
||||
nodeExtent?: CoordinateExtent;
|
||||
onError?: OnError;
|
||||
}): { position: XYPosition; positionAbsolute: XYPosition } {
|
||||
const node = nodeLookup.get(nodeId)!;
|
||||
const parentNode = node.parentId ? nodeLookup.get(node.parentId) : undefined;
|
||||
const { x: parentX, y: parentY } = parentNode ? parentNode.internals.positionAbsolute : { x: 0, y: 0 };
|
||||
const origin = node.origin ?? nodeOrigin;
|
||||
|
||||
if (node.parentNode) {
|
||||
parentNode = nodes.find((n) => n.id === node.parentNode) || null;
|
||||
parentPos = parentNode
|
||||
? getNodePositionWithOrigin(parentNode, parentNode.origin || nodeOrigin).positionAbsolute
|
||||
: parentPos;
|
||||
}
|
||||
let currentExtent = clampNodeExtent(node, node.extent || nodeExtent);
|
||||
|
||||
if (node.extent === 'parent' && !node.expandParent) {
|
||||
if (node.parentNode && node.width && node.height) {
|
||||
const currNodeOrigin = node.origin || nodeOrigin;
|
||||
|
||||
currentExtent =
|
||||
parentNode && isNumeric(parentNode.width) && isNumeric(parentNode.height)
|
||||
? [
|
||||
[parentPos.x + node.width * currNodeOrigin[0], parentPos.y + node.height * currNodeOrigin[1]],
|
||||
[
|
||||
parentPos.x + parentNode.width - node.width + node.width * currNodeOrigin[0],
|
||||
parentPos.y + parentNode.height - node.height + node.height * currNodeOrigin[1],
|
||||
],
|
||||
]
|
||||
: currentExtent;
|
||||
} else {
|
||||
if (!parentNode) {
|
||||
onError?.('005', errorMessages['error005']());
|
||||
currentExtent = clampedNodeExtent;
|
||||
} else {
|
||||
const nodeWidth = node.measured.width;
|
||||
const nodeHeight = node.measured.height;
|
||||
const parentWidth = parentNode.measured.width;
|
||||
const parentHeight = parentNode.measured.height;
|
||||
|
||||
if (nodeWidth && nodeHeight && parentWidth && parentHeight) {
|
||||
currentExtent = [
|
||||
[parentX, parentY],
|
||||
[parentX + parentWidth - nodeWidth, parentY + parentHeight - nodeHeight],
|
||||
];
|
||||
}
|
||||
}
|
||||
} else if (node.extent && node.parentNode && node.extent !== 'parent') {
|
||||
} else if (parentNode && isCoordinateExtent(node.extent)) {
|
||||
currentExtent = [
|
||||
[node.extent[0][0] + parentPos.x, node.extent[0][1] + parentPos.y],
|
||||
[node.extent[1][0] + parentPos.x, node.extent[1][1] + parentPos.y],
|
||||
[node.extent[0][0] + parentX, node.extent[0][1] + parentY],
|
||||
[node.extent[1][0] + parentX, node.extent[1][1] + parentY],
|
||||
];
|
||||
}
|
||||
|
||||
const positionAbsolute =
|
||||
currentExtent && currentExtent !== 'parent'
|
||||
? clampPosition(nextPosition, currentExtent as CoordinateExtent)
|
||||
: nextPosition;
|
||||
const positionAbsolute = isCoordinateExtent(currentExtent)
|
||||
? clampPosition(nextPosition, currentExtent)
|
||||
: nextPosition;
|
||||
|
||||
return {
|
||||
position: {
|
||||
x: positionAbsolute.x - parentPos.x,
|
||||
y: positionAbsolute.y - parentPos.y,
|
||||
// TODO: is there a better way to do this?
|
||||
x: positionAbsolute.x - parentX + node.measured.width! * origin[0],
|
||||
y: positionAbsolute.y - parentY + node.measured.height! * origin[1],
|
||||
},
|
||||
positionAbsolute,
|
||||
};
|
||||
}
|
||||
|
||||
// helper function to get arrays of nodes and edges that can be deleted
|
||||
// you can pass in a list of nodes and edges that should be deleted
|
||||
// and the function only returns elements that are deletable and also handles connected nodes and child nodes
|
||||
export function getElementsToRemove<NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>({
|
||||
nodesToRemove,
|
||||
edgesToRemove,
|
||||
/**
|
||||
* Pass in nodes & edges to delete, get arrays of nodes and edges that actually can be deleted
|
||||
* @internal
|
||||
* @param param.nodesToRemove - The nodes to remove
|
||||
* @param param.edgesToRemove - The edges to remove
|
||||
* @param param.nodes - All nodes
|
||||
* @param param.edges - All edges
|
||||
* @param param.onBeforeDelete - Callback to check which nodes and edges can be deleted
|
||||
* @returns nodes: nodes that can be deleted, edges: edges that can be deleted
|
||||
*/
|
||||
export async function getElementsToRemove<NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>({
|
||||
nodesToRemove = [],
|
||||
edgesToRemove = [],
|
||||
nodes,
|
||||
edges,
|
||||
onBeforeDelete,
|
||||
}: {
|
||||
nodesToRemove: Partial<NodeType>[];
|
||||
edgesToRemove: Partial<EdgeType>[];
|
||||
nodes: NodeType[];
|
||||
edges: EdgeType[];
|
||||
}): {
|
||||
matchingNodes: NodeType[];
|
||||
matchingEdges: EdgeType[];
|
||||
} {
|
||||
const nodeIds = nodesToRemove.map((node) => node.id);
|
||||
const edgeIds = edgesToRemove.map((edge) => edge.id);
|
||||
onBeforeDelete?: OnBeforeDeleteBase<NodeType, EdgeType>;
|
||||
}): Promise<{
|
||||
nodes: NodeType[];
|
||||
edges: EdgeType[];
|
||||
}> {
|
||||
const nodeIds = new Set(nodesToRemove.map((node) => node.id));
|
||||
const matchingNodes: NodeType[] = [];
|
||||
|
||||
const matchingNodes = nodes.reduce<NodeType[]>((res, node) => {
|
||||
const parentHit = !nodeIds.includes(node.id) && node.parentNode && res.find((n) => n.id === node.parentNode);
|
||||
const deletable = typeof node.deletable === 'boolean' ? node.deletable : true;
|
||||
if (deletable && (nodeIds.includes(node.id) || parentHit)) {
|
||||
res.push(node);
|
||||
for (const node of nodes) {
|
||||
if (node.deletable === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return res;
|
||||
}, []);
|
||||
const deletableEdges = edges.filter((e) => (typeof e.deletable === 'boolean' ? e.deletable : true));
|
||||
const initialHitEdges = deletableEdges.filter((e) => edgeIds.includes(e.id));
|
||||
const connectedEdges = getConnectedEdgesBase<NodeType, EdgeType>(matchingNodes, deletableEdges);
|
||||
const matchingEdges = [...initialHitEdges, ...connectedEdges];
|
||||
const isIncluded = nodeIds.has(node.id);
|
||||
const parentHit = !isIncluded && node.parentId && matchingNodes.find((n) => n.id === node.parentId);
|
||||
|
||||
return {
|
||||
matchingEdges,
|
||||
matchingNodes,
|
||||
};
|
||||
if (isIncluded || parentHit) {
|
||||
matchingNodes.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
const edgeIds = new Set(edgesToRemove.map((edge) => edge.id));
|
||||
const deletableEdges = edges.filter((edge) => edge.deletable !== false);
|
||||
const connectedEdges = getConnectedEdges(matchingNodes, deletableEdges);
|
||||
const matchingEdges: EdgeType[] = connectedEdges;
|
||||
|
||||
for (const edge of deletableEdges) {
|
||||
const isIncluded = edgeIds.has(edge.id);
|
||||
|
||||
if (isIncluded && !matchingEdges.find((e) => e.id === edge.id)) {
|
||||
matchingEdges.push(edge);
|
||||
}
|
||||
}
|
||||
|
||||
if (!onBeforeDelete) {
|
||||
return {
|
||||
edges: matchingEdges,
|
||||
nodes: matchingNodes,
|
||||
};
|
||||
}
|
||||
|
||||
const onBeforeDeleteResult = await onBeforeDelete({
|
||||
nodes: matchingNodes,
|
||||
edges: matchingEdges,
|
||||
});
|
||||
|
||||
if (typeof onBeforeDeleteResult === 'boolean') {
|
||||
return onBeforeDeleteResult ? { edges: matchingEdges, nodes: matchingNodes } : { edges: [], nodes: [] };
|
||||
}
|
||||
|
||||
return onBeforeDeleteResult;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
export * from './connections';
|
||||
export * from './dom';
|
||||
export * from './edges';
|
||||
export * from './graph';
|
||||
export * from './general';
|
||||
export * from './marker';
|
||||
export * from './node-toolbar';
|
||||
export * from './store';
|
||||
export * from './types';
|
||||
export * from './shallow-node-data';
|
||||
|
||||
@@ -19,21 +19,32 @@ export function getMarkerId(marker: EdgeMarkerType | undefined, id?: string | nu
|
||||
|
||||
export function createMarkerIds(
|
||||
edges: EdgeBase[],
|
||||
{ id, defaultColor }: { id?: string | null; defaultColor?: string }
|
||||
{
|
||||
id,
|
||||
defaultColor,
|
||||
defaultMarkerStart,
|
||||
defaultMarkerEnd,
|
||||
}: {
|
||||
id?: string | null;
|
||||
defaultColor?: string;
|
||||
defaultMarkerStart?: EdgeMarkerType;
|
||||
defaultMarkerEnd?: EdgeMarkerType;
|
||||
}
|
||||
) {
|
||||
const ids: string[] = [];
|
||||
const ids = new Set<string>();
|
||||
|
||||
return edges
|
||||
.reduce<MarkerProps[]>((markers, edge) => {
|
||||
[edge.markerStart, edge.markerEnd].forEach((marker) => {
|
||||
[edge.markerStart || defaultMarkerStart, edge.markerEnd || defaultMarkerEnd].forEach((marker) => {
|
||||
if (marker && typeof marker === 'object') {
|
||||
const markerId = getMarkerId(marker, id);
|
||||
if (!ids.includes(markerId)) {
|
||||
if (!ids.has(markerId)) {
|
||||
markers.push({ id: markerId, color: marker.color || defaultColor, ...marker });
|
||||
ids.push(markerId);
|
||||
ids.add(markerId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return markers;
|
||||
}, [])
|
||||
.sort((a, b) => a.id.localeCompare(b.id));
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Position, type Rect, type Viewport, type Align } from '../';
|
||||
|
||||
export function getNodeToolbarTransform(
|
||||
nodeRect: Rect,
|
||||
viewport: Viewport,
|
||||
position: Position,
|
||||
offset: number,
|
||||
align: Align
|
||||
): string {
|
||||
let alignmentOffset = 0.5;
|
||||
|
||||
if (align === 'start') {
|
||||
alignmentOffset = 0;
|
||||
} else if (align === 'end') {
|
||||
alignmentOffset = 1;
|
||||
}
|
||||
|
||||
// position === Position.Top
|
||||
// we set the x any y position of the toolbar based on the nodes position
|
||||
let pos = [
|
||||
(nodeRect.x + nodeRect.width * alignmentOffset) * viewport.zoom + viewport.x,
|
||||
nodeRect.y * viewport.zoom + viewport.y - offset,
|
||||
];
|
||||
// and than shift it based on the alignment. The shift values are in %.
|
||||
let shift = [-100 * alignmentOffset, -100];
|
||||
|
||||
switch (position) {
|
||||
case Position.Right:
|
||||
pos = [
|
||||
(nodeRect.x + nodeRect.width) * viewport.zoom + viewport.x + offset,
|
||||
(nodeRect.y + nodeRect.height * alignmentOffset) * viewport.zoom + viewport.y,
|
||||
];
|
||||
shift = [0, -100 * alignmentOffset];
|
||||
break;
|
||||
case Position.Bottom:
|
||||
pos[1] = (nodeRect.y + nodeRect.height) * viewport.zoom + viewport.y + offset;
|
||||
shift[1] = 0;
|
||||
break;
|
||||
case Position.Left:
|
||||
pos = [
|
||||
nodeRect.x * viewport.zoom + viewport.x - offset,
|
||||
(nodeRect.y + nodeRect.height * alignmentOffset) * viewport.zoom + viewport.y,
|
||||
];
|
||||
shift = [-100, -100 * alignmentOffset];
|
||||
break;
|
||||
}
|
||||
|
||||
return `translate(${pos[0]}px, ${pos[1]}px) translate(${shift[0]}%, ${shift[1]}%)`;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { NodeBase } from '../types';
|
||||
|
||||
type NodeData = Pick<NodeBase, 'id' | 'type' | 'data'>;
|
||||
|
||||
export function shallowNodeData(a: NodeData | NodeData[] | null, b: NodeData | NodeData[] | null) {
|
||||
if (a === null || b === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const _a = Array.isArray(a) ? a : [a];
|
||||
const _b = Array.isArray(b) ? b : [b];
|
||||
|
||||
if (_a.length !== _b.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < _a.length; i++) {
|
||||
if (_a[i].id !== _b[i].id || _a[i].type !== _b[i].type || !Object.is(_a[i].data, _b[i].data)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
+286
-119
@@ -1,181 +1,328 @@
|
||||
import { internalsSymbol } from '../constants';
|
||||
import {
|
||||
NodeBase,
|
||||
CoordinateExtent,
|
||||
Dimensions,
|
||||
NodeDimensionUpdate,
|
||||
InternalNodeUpdate,
|
||||
NodeOrigin,
|
||||
PanZoomInstance,
|
||||
Transform,
|
||||
XYPosition,
|
||||
XYZPosition,
|
||||
ConnectionLookup,
|
||||
EdgeBase,
|
||||
EdgeLookup,
|
||||
InternalNodeBase,
|
||||
NodeLookup,
|
||||
Rect,
|
||||
NodeDimensionChange,
|
||||
NodePositionChange,
|
||||
ParentLookup,
|
||||
} from '../types';
|
||||
import { getDimensions, getHandleBounds } from './dom';
|
||||
import { isNumeric } from './general';
|
||||
import { getBoundsOfRects, getNodeDimensions, isNumeric, nodeToRect } from './general';
|
||||
import { getNodePositionWithOrigin } from './graph';
|
||||
import { ParentExpandChild } from './types';
|
||||
|
||||
type ParentNodes = Record<string, boolean>;
|
||||
const defaultOptions = {
|
||||
nodeOrigin: [0, 0] as NodeOrigin,
|
||||
elevateNodesOnSelect: true,
|
||||
defaults: {},
|
||||
};
|
||||
const adoptUserNodesDefaultOptions = {
|
||||
...defaultOptions,
|
||||
checkEquality: true,
|
||||
};
|
||||
|
||||
export function updateAbsolutePositions<NodeType extends NodeBase>(
|
||||
nodes: NodeType[],
|
||||
nodeOrigin: NodeOrigin = [0, 0],
|
||||
parentNodes?: ParentNodes
|
||||
nodeLookup: NodeLookup<InternalNodeBase<NodeType>>,
|
||||
parentLookup: ParentLookup<InternalNodeBase<NodeType>>,
|
||||
options?: UpdateNodesOptions<NodeType>
|
||||
) {
|
||||
return nodes.map((node) => {
|
||||
if (node.parentNode && !nodes.find((n) => n.id === node.parentNode)) {
|
||||
throw new Error(`Parent node ${node.parentNode} not found`);
|
||||
const _options = { ...defaultOptions, ...options };
|
||||
for (const node of nodeLookup.values()) {
|
||||
if (!node.parentId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.parentNode || parentNodes?.[node.id]) {
|
||||
const parentNode = node.parentNode ? nodes.find((n) => n.id === node.parentNode) : null;
|
||||
const { x, y, z } = calculateXYZPosition(
|
||||
node,
|
||||
nodes,
|
||||
{
|
||||
...node.position,
|
||||
z: node[internalsSymbol]?.z ?? 0,
|
||||
},
|
||||
parentNode?.origin || nodeOrigin
|
||||
);
|
||||
|
||||
node.positionAbsolute = {
|
||||
x,
|
||||
y,
|
||||
};
|
||||
|
||||
node[internalsSymbol]!.z = z;
|
||||
|
||||
if (parentNodes?.[node.id]) {
|
||||
node[internalsSymbol]!.isParent = true;
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
});
|
||||
updateChildPosition(node, nodeLookup, parentLookup, _options);
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateNodesOptions<NodeType extends NodeBase> = {
|
||||
nodeOrigin?: NodeOrigin;
|
||||
elevateNodesOnSelect?: boolean;
|
||||
defaults?: Partial<NodeType>;
|
||||
checkEquality?: boolean;
|
||||
};
|
||||
|
||||
export function updateNodes<NodeType extends NodeBase>(
|
||||
export function adoptUserNodes<NodeType extends NodeBase>(
|
||||
nodes: NodeType[],
|
||||
storeNodes: NodeType[],
|
||||
options: UpdateNodesOptions<NodeType> = {
|
||||
nodeOrigin: [0, 0] as NodeOrigin,
|
||||
elevateNodesOnSelect: true,
|
||||
defaults: {},
|
||||
}
|
||||
): NodeType[] {
|
||||
const parentNodes: ParentNodes = {};
|
||||
nodeLookup: NodeLookup<InternalNodeBase<NodeType>>,
|
||||
parentLookup: ParentLookup<InternalNodeBase<NodeType>>,
|
||||
options?: UpdateNodesOptions<NodeType>
|
||||
) {
|
||||
const _options = { ...adoptUserNodesDefaultOptions, ...options };
|
||||
const tmpLookup = new Map(nodeLookup);
|
||||
nodeLookup.clear();
|
||||
parentLookup.clear();
|
||||
|
||||
const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0;
|
||||
|
||||
const nextNodes = nodes.map((n) => {
|
||||
const currentStoreNode = storeNodes.find((storeNode) => n.id === storeNode.id);
|
||||
const node: NodeType = {
|
||||
...options.defaults,
|
||||
...n,
|
||||
positionAbsolute: n.position,
|
||||
width: n.width || currentStoreNode?.width,
|
||||
height: n.height || currentStoreNode?.height,
|
||||
};
|
||||
const z = (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0);
|
||||
const currInternals = n?.[internalsSymbol] || currentStoreNode?.[internalsSymbol];
|
||||
|
||||
if (node.parentNode) {
|
||||
parentNodes[node.parentNode] = true;
|
||||
for (const userNode of nodes) {
|
||||
let internalNode = tmpLookup.get(userNode.id);
|
||||
if (_options.checkEquality && userNode === internalNode?.internals.userNode) {
|
||||
nodeLookup.set(userNode.id, internalNode);
|
||||
} else {
|
||||
internalNode = {
|
||||
..._options.defaults,
|
||||
...userNode,
|
||||
measured: {
|
||||
width: userNode.measured?.width,
|
||||
height: userNode.measured?.height,
|
||||
},
|
||||
internals: {
|
||||
positionAbsolute: getNodePositionWithOrigin(userNode, _options.nodeOrigin),
|
||||
handleBounds: internalNode?.internals.handleBounds,
|
||||
z: calculateZ(userNode, selectedNodeZ),
|
||||
userNode,
|
||||
},
|
||||
};
|
||||
nodeLookup.set(userNode.id, internalNode);
|
||||
}
|
||||
|
||||
Object.defineProperty(node, internalsSymbol, {
|
||||
enumerable: false,
|
||||
value: {
|
||||
handleBounds: currInternals?.handleBounds,
|
||||
z,
|
||||
},
|
||||
});
|
||||
|
||||
return node;
|
||||
});
|
||||
|
||||
const nodesWithPositions = updateAbsolutePositions(nextNodes, options.nodeOrigin, parentNodes);
|
||||
|
||||
return nodesWithPositions;
|
||||
if (userNode.parentId) {
|
||||
updateChildPosition(internalNode, nodeLookup, parentLookup, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function calculateXYZPosition<NodeType extends NodeBase>(
|
||||
node: NodeType,
|
||||
nodes: NodeType[],
|
||||
result: XYZPosition,
|
||||
nodeOrigin: NodeOrigin
|
||||
): XYZPosition {
|
||||
if (!node.parentNode) {
|
||||
return result;
|
||||
function updateChildPosition<NodeType extends NodeBase>(
|
||||
node: InternalNodeBase<NodeType>,
|
||||
nodeLookup: NodeLookup<InternalNodeBase<NodeType>>,
|
||||
parentLookup: ParentLookup<InternalNodeBase<NodeType>>,
|
||||
options?: UpdateNodesOptions<NodeType>
|
||||
) {
|
||||
const _options = { ...defaultOptions, ...options };
|
||||
|
||||
const parentId = node.parentId!;
|
||||
const parentNode = nodeLookup.get(parentId);
|
||||
if (!parentNode) {
|
||||
throw new Error(`Parent node ${parentId} not found`);
|
||||
}
|
||||
|
||||
const parentNode = nodes.find((n) => n.id === node.parentNode)!;
|
||||
const parentNodePosition = getNodePositionWithOrigin(parentNode, parentNode?.origin || nodeOrigin);
|
||||
// update the parentLookup
|
||||
const childNodes = parentLookup.get(parentId);
|
||||
if (childNodes) {
|
||||
childNodes.set(node.id, node);
|
||||
} else {
|
||||
parentLookup.set(parentId, new Map([[node.id, node]]));
|
||||
}
|
||||
|
||||
return calculateXYZPosition(
|
||||
parentNode,
|
||||
nodes,
|
||||
{
|
||||
x: (result.x ?? 0) + parentNodePosition.x,
|
||||
y: (result.y ?? 0) + parentNodePosition.y,
|
||||
z: (parentNode[internalsSymbol]?.z ?? 0) > (result.z ?? 0) ? parentNode[internalsSymbol]?.z ?? 0 : result.z ?? 0,
|
||||
},
|
||||
parentNode.origin || nodeOrigin
|
||||
);
|
||||
const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0;
|
||||
|
||||
const { x, y, z } = calculateChildXYZ(node, parentNode, _options.nodeOrigin!, selectedNodeZ);
|
||||
|
||||
const currPosition = node.internals.positionAbsolute;
|
||||
const positionChanged = x !== currPosition.x || y !== currPosition.y;
|
||||
|
||||
if (positionChanged || z !== node.internals.z) {
|
||||
node.internals = {
|
||||
...node.internals,
|
||||
positionAbsolute: positionChanged ? { x, y } : currPosition,
|
||||
z,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function updateNodeDimensions(
|
||||
updates: NodeDimensionUpdate[],
|
||||
nodes: NodeBase[],
|
||||
function calculateZ(node: NodeBase, selectedNodeZ: number) {
|
||||
return (isNumeric(node.zIndex) ? node.zIndex : 0) + (node.selected ? selectedNodeZ : 0);
|
||||
}
|
||||
|
||||
function calculateChildXYZ<NodeType extends NodeBase>(
|
||||
childNode: InternalNodeBase<NodeType>,
|
||||
parentNode: InternalNodeBase<NodeType>,
|
||||
nodeOrigin: NodeOrigin,
|
||||
selectedNodeZ: number
|
||||
) {
|
||||
const position = getNodePositionWithOrigin(childNode, nodeOrigin);
|
||||
const childZ = calculateZ(childNode, selectedNodeZ);
|
||||
const parentZ = parentNode.internals.z ?? 0;
|
||||
|
||||
return {
|
||||
x: parentNode.internals.positionAbsolute.x + position.x,
|
||||
y: parentNode.internals.positionAbsolute.y + position.y,
|
||||
z: parentZ > childZ ? parentZ : childZ,
|
||||
};
|
||||
}
|
||||
|
||||
export function handleExpandParent(
|
||||
children: ParentExpandChild[],
|
||||
nodeLookup: NodeLookup,
|
||||
parentLookup: ParentLookup,
|
||||
nodeOrigin: NodeOrigin = [0, 0]
|
||||
): (NodeDimensionChange | NodePositionChange)[] {
|
||||
const changes: (NodeDimensionChange | NodePositionChange)[] = [];
|
||||
const parentExpansions = new Map<string, { expandedRect: Rect; parent: InternalNodeBase }>();
|
||||
|
||||
// determine the expanded rectangle the child nodes would take for each parent
|
||||
for (const child of children) {
|
||||
const parent = nodeLookup.get(child.parentId);
|
||||
if (!parent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parentRect = parentExpansions.get(child.parentId)?.expandedRect ?? nodeToRect(parent);
|
||||
const expandedRect = getBoundsOfRects(parentRect, child.rect);
|
||||
|
||||
parentExpansions.set(child.parentId, { expandedRect, parent });
|
||||
}
|
||||
|
||||
if (parentExpansions.size > 0) {
|
||||
parentExpansions.forEach(({ expandedRect, parent }, parentId) => {
|
||||
// determine the position & dimensions of the parent
|
||||
const positionAbsolute = parent.internals.positionAbsolute;
|
||||
const dimensions = getNodeDimensions(parent);
|
||||
const origin = parent.origin ?? nodeOrigin;
|
||||
|
||||
// determine how much the parent expands in width and position
|
||||
const xChange =
|
||||
expandedRect.x < positionAbsolute.x ? Math.round(Math.abs(positionAbsolute.x - expandedRect.x)) : 0;
|
||||
const yChange =
|
||||
expandedRect.y < positionAbsolute.y ? Math.round(Math.abs(positionAbsolute.y - expandedRect.y)) : 0;
|
||||
|
||||
const newWidth = Math.max(dimensions.width, Math.round(expandedRect.width));
|
||||
const newHeight = Math.max(dimensions.height, Math.round(expandedRect.height));
|
||||
|
||||
const widthChange = (newWidth - dimensions.width) * origin[0];
|
||||
const heightChange = (newHeight - dimensions.height) * origin[1];
|
||||
|
||||
// We need to correct the position of the parent node if the origin is not [0,0]
|
||||
if (xChange > 0 || yChange > 0 || widthChange || heightChange) {
|
||||
changes.push({
|
||||
id: parentId,
|
||||
type: 'position',
|
||||
position: {
|
||||
x: parent.position.x - xChange + widthChange,
|
||||
y: parent.position.y - yChange + heightChange,
|
||||
},
|
||||
});
|
||||
|
||||
// We move all child nodes in the oppsite direction
|
||||
// so the x,y changes of the parent do not move the children
|
||||
parentLookup.get(parentId)?.forEach((childNode) => {
|
||||
if (!children.some((child) => child.id === childNode.id)) {
|
||||
changes.push({
|
||||
id: childNode.id,
|
||||
type: 'position',
|
||||
position: {
|
||||
x: childNode.position.x + xChange,
|
||||
y: childNode.position.y + yChange,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// We need to correct the dimensions of the parent node if the origin is not [0,0]
|
||||
if (dimensions.width < expandedRect.width || dimensions.height < expandedRect.height || xChange || yChange) {
|
||||
changes.push({
|
||||
id: parentId,
|
||||
type: 'dimensions',
|
||||
setAttributes: true,
|
||||
dimensions: {
|
||||
width: newWidth + (xChange ? origin[0] * xChange - widthChange : 0),
|
||||
height: newHeight + (yChange ? origin[1] * yChange - heightChange : 0),
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
export function updateNodeInternals<NodeType extends InternalNodeBase>(
|
||||
updates: Map<string, InternalNodeUpdate>,
|
||||
nodeLookup: NodeLookup<NodeType>,
|
||||
parentLookup: ParentLookup<NodeType>,
|
||||
domNode: HTMLElement | null,
|
||||
nodeOrigin?: NodeOrigin,
|
||||
onUpdate?: (id: string, dimensions: Dimensions) => void
|
||||
): NodeBase[] | null {
|
||||
nodeOrigin?: NodeOrigin
|
||||
): { changes: (NodeDimensionChange | NodePositionChange)[]; updatedInternals: boolean } {
|
||||
const viewportNode = domNode?.querySelector('.xyflow__viewport');
|
||||
let updatedInternals = false;
|
||||
|
||||
if (!viewportNode) {
|
||||
return null;
|
||||
return { changes: [], updatedInternals };
|
||||
}
|
||||
|
||||
const changes: (NodeDimensionChange | NodePositionChange)[] = [];
|
||||
const style = window.getComputedStyle(viewportNode);
|
||||
const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform);
|
||||
// in this array we collect nodes, that might trigger changes (like expanding parent)
|
||||
const parentExpandChildren: ParentExpandChild[] = [];
|
||||
|
||||
const nextNodes = nodes.map((node) => {
|
||||
const update = updates.find((u) => u.id === node.id);
|
||||
if (update) {
|
||||
for (const update of updates.values()) {
|
||||
const node = nodeLookup.get(update.id);
|
||||
if (!node) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.hidden) {
|
||||
node.internals = {
|
||||
...node.internals,
|
||||
handleBounds: undefined,
|
||||
};
|
||||
updatedInternals = true;
|
||||
} else {
|
||||
const dimensions = getDimensions(update.nodeElement);
|
||||
const dimensionChanged = node.measured.width !== dimensions.width || node.measured.height !== dimensions.height;
|
||||
const doUpdate = !!(
|
||||
dimensions.width &&
|
||||
dimensions.height &&
|
||||
(node.width !== dimensions.width || node.height !== dimensions.height || update.forceUpdate)
|
||||
(dimensionChanged || !node.internals.handleBounds || update.force)
|
||||
);
|
||||
|
||||
if (doUpdate) {
|
||||
onUpdate?.(node.id, dimensions);
|
||||
const nodeBounds = update.nodeElement.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
...node,
|
||||
...dimensions,
|
||||
[internalsSymbol]: {
|
||||
...node[internalsSymbol],
|
||||
handleBounds: {
|
||||
source: getHandleBounds('.source', update.nodeElement, zoom, node.origin || nodeOrigin),
|
||||
target: getHandleBounds('.target', update.nodeElement, zoom, node.origin || nodeOrigin),
|
||||
},
|
||||
node.measured = dimensions;
|
||||
node.internals = {
|
||||
...node.internals,
|
||||
positionAbsolute: getNodePositionWithOrigin(node, nodeOrigin),
|
||||
handleBounds: {
|
||||
source: getHandleBounds('source', update.nodeElement, nodeBounds, zoom, node.id),
|
||||
target: getHandleBounds('target', update.nodeElement, nodeBounds, zoom, node.id),
|
||||
},
|
||||
};
|
||||
if (node.parentId) {
|
||||
updateChildPosition(node, nodeLookup, parentLookup, { nodeOrigin });
|
||||
}
|
||||
|
||||
updatedInternals = true;
|
||||
|
||||
if (dimensionChanged) {
|
||||
changes.push({
|
||||
id: node.id,
|
||||
type: 'dimensions',
|
||||
dimensions,
|
||||
});
|
||||
|
||||
if (node.expandParent && node.parentId) {
|
||||
parentExpandChildren.push({
|
||||
id: node.id,
|
||||
parentId: node.parentId,
|
||||
rect: nodeToRect(node, nodeOrigin),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
});
|
||||
if (parentExpandChildren.length > 0) {
|
||||
const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup, nodeOrigin);
|
||||
changes.push(...parentExpandChanges);
|
||||
}
|
||||
|
||||
return nextNodes;
|
||||
return { changes, updatedInternals };
|
||||
}
|
||||
|
||||
export function panBy({
|
||||
@@ -216,3 +363,23 @@ export function panBy({
|
||||
|
||||
return transformChanged;
|
||||
}
|
||||
|
||||
export function updateConnectionLookup(connectionLookup: ConnectionLookup, edgeLookup: EdgeLookup, edges: EdgeBase[]) {
|
||||
connectionLookup.clear();
|
||||
edgeLookup.clear();
|
||||
|
||||
for (const edge of edges) {
|
||||
const { source, target, sourceHandle = null, targetHandle = null } = edge;
|
||||
|
||||
const sourceKey = `${source}-source-${sourceHandle}`;
|
||||
const targetKey = `${target}-target-${targetHandle}`;
|
||||
|
||||
const prevSource = connectionLookup.get(sourceKey) || new Map();
|
||||
const prevTarget = connectionLookup.get(targetKey) || new Map();
|
||||
const connection = { edgeId: edge.id, source, target, sourceHandle, targetHandle };
|
||||
|
||||
edgeLookup.set(edge.id, edge);
|
||||
connectionLookup.set(sourceKey, prevSource.set(`${target}-${targetHandle}`, connection));
|
||||
connectionLookup.set(targetKey, prevTarget.set(`${source}-${sourceHandle}`, connection));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
import { Rect } from '../types';
|
||||
|
||||
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
||||
|
||||
export type ParentExpandChild = { id: string; parentId: string; rect: Rect };
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { drag } from 'd3-drag';
|
||||
import { select } from 'd3-selection';
|
||||
import { select, type Selection } from 'd3-selection';
|
||||
|
||||
import {
|
||||
calcAutoPan,
|
||||
getEventPosition,
|
||||
getPointerPosition,
|
||||
calcNextPosition,
|
||||
calculateNodePosition,
|
||||
snapPosition,
|
||||
getRectOfNodes,
|
||||
getInternalNodesBounds,
|
||||
rectToBox,
|
||||
} from '../utils';
|
||||
import { getDragItems, getEventHandlerParams, hasSelector, wrapSelectionDragFunc } from './utils';
|
||||
import { getDragItems, getEventHandlerParams, hasSelector } from './utils';
|
||||
import type {
|
||||
NodeBase,
|
||||
NodeDragItem,
|
||||
@@ -23,16 +23,22 @@ import type {
|
||||
SnapGrid,
|
||||
Transform,
|
||||
PanBy,
|
||||
OnNodeDrag,
|
||||
OnSelectionDrag,
|
||||
UpdateNodePositions,
|
||||
Box,
|
||||
InternalNodeBase,
|
||||
} from '../types';
|
||||
|
||||
export type OnDrag = (event: MouseEvent, dragItems: NodeDragItem[], node: NodeBase, nodes: NodeBase[]) => void;
|
||||
export type OnDrag = (
|
||||
event: MouseEvent,
|
||||
dragItems: Map<string, NodeDragItem>,
|
||||
node: NodeBase,
|
||||
nodes: NodeBase[]
|
||||
) => void;
|
||||
|
||||
type StoreItems = {
|
||||
type StoreItems<OnNodeDrag> = {
|
||||
nodes: NodeBase[];
|
||||
nodeLookup: Map<string, InternalNodeBase>;
|
||||
edges: EdgeBase[];
|
||||
nodeExtent: CoordinateExtent;
|
||||
snapGrid: SnapGrid;
|
||||
@@ -55,15 +61,16 @@ type StoreItems = {
|
||||
onSelectionDrag?: OnSelectionDrag;
|
||||
onSelectionDragStop?: OnSelectionDrag;
|
||||
updateNodePositions: UpdateNodePositions;
|
||||
autoPanSpeed?: number;
|
||||
};
|
||||
|
||||
export type XYDragParams = {
|
||||
domNode: Element;
|
||||
getStoreItems: () => StoreItems;
|
||||
export type XYDragParams<OnNodeDrag> = {
|
||||
getStoreItems: () => StoreItems<OnNodeDrag>;
|
||||
onDragStart?: OnDrag;
|
||||
onDrag?: OnDrag;
|
||||
onDragStop?: OnDrag;
|
||||
onNodeMouseDown?: (id: string) => void;
|
||||
autoPanSpeed?: number;
|
||||
};
|
||||
|
||||
export type XYDragInstance = {
|
||||
@@ -79,30 +86,30 @@ export type DragUpdateParams = {
|
||||
domNode: Element;
|
||||
};
|
||||
|
||||
export function XYDrag({
|
||||
domNode,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => void | undefined>({
|
||||
onNodeMouseDown,
|
||||
getStoreItems,
|
||||
onDragStart,
|
||||
onDrag,
|
||||
onDragStop,
|
||||
}: XYDragParams): XYDragInstance {
|
||||
}: XYDragParams<OnNodeDrag>): XYDragInstance {
|
||||
let lastPos: { x: number | null; y: number | null } = { x: null, y: null };
|
||||
let autoPanId = 0;
|
||||
let dragItems: NodeDragItem[] = [];
|
||||
let dragItems = new Map<string, NodeDragItem>();
|
||||
let autoPanStarted = false;
|
||||
let mousePosition: XYPosition = { x: 0, y: 0 };
|
||||
let dragEvent: MouseEvent | null = null;
|
||||
let containerBounds: DOMRect | null = null;
|
||||
let dragStarted = false;
|
||||
|
||||
const d3Selection = select(domNode);
|
||||
let d3Selection: Selection<Element, unknown, null, undefined> | null = null;
|
||||
let abortDrag = false; // prevents unintentional dragging on multitouch
|
||||
|
||||
// public functions
|
||||
function update({ noDragClassName, handleSelector, domNode, isSelectable, nodeId }: DragUpdateParams) {
|
||||
function updateNodes({ x, y }: XYPosition) {
|
||||
d3Selection = select(domNode);
|
||||
function updateNodes({ x, y }: XYPosition, dragEvent: MouseEvent | null) {
|
||||
const {
|
||||
nodes,
|
||||
nodeLookup,
|
||||
nodeExtent,
|
||||
snapGrid,
|
||||
snapToGrid,
|
||||
@@ -118,59 +125,73 @@ export function XYDrag({
|
||||
let hasChange = false;
|
||||
let nodesBox: Box = { x: 0, y: 0, x2: 0, y2: 0 };
|
||||
|
||||
if (dragItems.length > 1 && nodeExtent) {
|
||||
const rect = getRectOfNodes(dragItems as unknown as NodeBase[], nodeOrigin);
|
||||
if (dragItems.size > 1 && nodeExtent) {
|
||||
const rect = getInternalNodesBounds(dragItems);
|
||||
nodesBox = rectToBox(rect);
|
||||
}
|
||||
|
||||
dragItems = dragItems.map((n) => {
|
||||
let nextPosition = { x: x - n.distance.x, y: y - n.distance.y };
|
||||
|
||||
for (const [id, dragItem] of dragItems) {
|
||||
let nextPosition = { x: x - dragItem.distance.x, y: y - dragItem.distance.y };
|
||||
if (snapToGrid) {
|
||||
nextPosition = snapPosition(nextPosition, snapGrid);
|
||||
}
|
||||
|
||||
// if there is selection with multiple nodes and a node extent is set, we need to adjust the node extent for each node
|
||||
// based on its position so that the node stays at it's position relative to the selection.
|
||||
const adjustedNodeExtent: CoordinateExtent = [
|
||||
let adjustedNodeExtent: CoordinateExtent = [
|
||||
[nodeExtent[0][0], nodeExtent[0][1]],
|
||||
[nodeExtent[1][0], nodeExtent[1][1]],
|
||||
];
|
||||
|
||||
if (dragItems.length > 1 && nodeExtent && !n.extent) {
|
||||
adjustedNodeExtent[0][0] = n.positionAbsolute.x - nodesBox.x + nodeExtent[0][0];
|
||||
adjustedNodeExtent[1][0] = n.positionAbsolute.x + (n.width ?? 0) - nodesBox.x2 + nodeExtent[1][0];
|
||||
if (dragItems.size > 1 && nodeExtent && !dragItem.extent) {
|
||||
const { positionAbsolute } = dragItem.internals;
|
||||
const x1 = positionAbsolute.x - nodesBox.x + nodeExtent[0][0];
|
||||
const x2 = positionAbsolute.x + dragItem.measured.width - nodesBox.x2 + nodeExtent[1][0];
|
||||
|
||||
adjustedNodeExtent[0][1] = n.positionAbsolute.y - nodesBox.y + nodeExtent[0][1];
|
||||
adjustedNodeExtent[1][1] = n.positionAbsolute.y + (n.height ?? 0) - nodesBox.y2 + nodeExtent[1][1];
|
||||
const y1 = positionAbsolute.y - nodesBox.y + nodeExtent[0][1];
|
||||
const y2 = positionAbsolute.y + dragItem.measured.height - nodesBox.y2 + nodeExtent[1][1];
|
||||
|
||||
adjustedNodeExtent = [
|
||||
[x1, y1],
|
||||
[x2, y2],
|
||||
];
|
||||
}
|
||||
|
||||
const updatedPos = calcNextPosition(n, nextPosition, nodes, adjustedNodeExtent, nodeOrigin, onError);
|
||||
const { position, positionAbsolute } = calculateNodePosition({
|
||||
nodeId: id,
|
||||
nextPosition,
|
||||
nodeLookup,
|
||||
nodeExtent: adjustedNodeExtent,
|
||||
nodeOrigin,
|
||||
onError,
|
||||
});
|
||||
|
||||
// we want to make sure that we only fire a change event when there is a change
|
||||
hasChange = hasChange || n.position.x !== updatedPos.position.x || n.position.y !== updatedPos.position.y;
|
||||
hasChange = hasChange || dragItem.position.x !== position.x || dragItem.position.y !== position.y;
|
||||
|
||||
n.position = updatedPos.position;
|
||||
n.positionAbsolute = updatedPos.positionAbsolute;
|
||||
|
||||
return n;
|
||||
});
|
||||
dragItem.position = position;
|
||||
dragItem.internals.positionAbsolute = positionAbsolute;
|
||||
}
|
||||
|
||||
if (!hasChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateNodePositions(dragItems, true, true);
|
||||
const onNodeOrSelectionDrag = nodeId ? onNodeDrag : wrapSelectionDragFunc(onSelectionDrag);
|
||||
updateNodePositions(dragItems, true);
|
||||
|
||||
if (dragEvent) {
|
||||
if (dragEvent && (onDrag || onNodeDrag || (!nodeId && onSelectionDrag))) {
|
||||
const [currentNode, currentNodes] = getEventHandlerParams({
|
||||
nodeId,
|
||||
dragItems,
|
||||
nodes,
|
||||
nodeLookup,
|
||||
});
|
||||
onDrag?.(dragEvent as MouseEvent, dragItems, currentNode, currentNodes);
|
||||
onNodeOrSelectionDrag?.(dragEvent as MouseEvent, currentNode, currentNodes);
|
||||
|
||||
onDrag?.(dragEvent, dragItems, currentNode, currentNodes);
|
||||
onNodeDrag?.(dragEvent, currentNode, currentNodes);
|
||||
|
||||
if (!nodeId) {
|
||||
onSelectionDrag?.(dragEvent, currentNodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,16 +200,16 @@ export function XYDrag({
|
||||
return;
|
||||
}
|
||||
|
||||
const [xMovement, yMovement] = calcAutoPan(mousePosition, containerBounds);
|
||||
const { transform, panBy, autoPanSpeed } = getStoreItems();
|
||||
|
||||
const [xMovement, yMovement] = calcAutoPan(mousePosition, containerBounds, autoPanSpeed);
|
||||
|
||||
if (xMovement !== 0 || yMovement !== 0) {
|
||||
const { transform, panBy } = getStoreItems();
|
||||
|
||||
lastPos.x = (lastPos.x ?? 0) - xMovement / transform[2];
|
||||
lastPos.y = (lastPos.y ?? 0) - yMovement / transform[2];
|
||||
|
||||
if (panBy({ x: xMovement, y: yMovement })) {
|
||||
updateNodes(lastPos as XYPosition);
|
||||
updateNodes(lastPos as XYPosition, null);
|
||||
}
|
||||
}
|
||||
autoPanId = requestAnimationFrame(autoPan);
|
||||
@@ -196,7 +217,7 @@ export function XYDrag({
|
||||
|
||||
function startDrag(event: UseDragEvent) {
|
||||
const {
|
||||
nodes,
|
||||
nodeLookup,
|
||||
multiSelectionActive,
|
||||
nodesDraggable,
|
||||
transform,
|
||||
@@ -211,7 +232,7 @@ export function XYDrag({
|
||||
dragStarted = true;
|
||||
|
||||
if ((!selectNodesOnDrag || !isSelectable) && !multiSelectionActive && nodeId) {
|
||||
if (!nodes.find((n) => n.id === nodeId)?.selected) {
|
||||
if (!nodeLookup.get(nodeId)?.selected) {
|
||||
// we need to reset selected nodes when selectNodesOnDrag=false
|
||||
unselectNodesAndEdges();
|
||||
}
|
||||
@@ -223,18 +244,21 @@ export function XYDrag({
|
||||
|
||||
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
|
||||
lastPos = pointerPos;
|
||||
dragItems = getDragItems(nodes, nodesDraggable, pointerPos, nodeId);
|
||||
dragItems = getDragItems(nodeLookup, nodesDraggable, pointerPos, nodeId);
|
||||
|
||||
const onNodeOrSelectionDragStart = nodeId ? onNodeDragStart : wrapSelectionDragFunc(onSelectionDragStart);
|
||||
|
||||
if (dragItems) {
|
||||
if (dragItems.size > 0 && (onDragStart || onNodeDragStart || (!nodeId && onSelectionDragStart))) {
|
||||
const [currentNode, currentNodes] = getEventHandlerParams({
|
||||
nodeId,
|
||||
dragItems,
|
||||
nodes,
|
||||
nodeLookup,
|
||||
});
|
||||
|
||||
onDragStart?.(event.sourceEvent as MouseEvent, dragItems, currentNode, currentNodes);
|
||||
onNodeOrSelectionDragStart?.(event.sourceEvent as MouseEvent, currentNode, currentNodes);
|
||||
onNodeDragStart?.(event.sourceEvent as MouseEvent, currentNode, currentNodes);
|
||||
|
||||
if (!nodeId) {
|
||||
onSelectionDragStart?.(event.sourceEvent as MouseEvent, currentNodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,6 +266,8 @@ export function XYDrag({
|
||||
.on('start', (event: UseDragEvent) => {
|
||||
const { domNode, nodeDragThreshold, transform, snapGrid, snapToGrid } = getStoreItems();
|
||||
|
||||
abortDrag = false;
|
||||
|
||||
if (nodeDragThreshold === 0) {
|
||||
startDrag(event);
|
||||
}
|
||||
@@ -255,6 +281,14 @@ export function XYDrag({
|
||||
const { autoPanOnNodeDrag, transform, snapGrid, snapToGrid, nodeDragThreshold } = getStoreItems();
|
||||
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
|
||||
|
||||
if (event.sourceEvent.type === 'touchmove' && event.sourceEvent.touches.length > 1) {
|
||||
abortDrag = true;
|
||||
}
|
||||
|
||||
if (abortDrag) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!autoPanStarted && autoPanOnNodeDrag && dragStarted) {
|
||||
autoPanStarted = true;
|
||||
autoPan();
|
||||
@@ -272,14 +306,14 @@ export function XYDrag({
|
||||
|
||||
// skip events without movement
|
||||
if ((lastPos.x !== pointerPos.xSnapped || lastPos.y !== pointerPos.ySnapped) && dragItems && dragStarted) {
|
||||
dragEvent = event.sourceEvent as MouseEvent;
|
||||
// dragEvent = event.sourceEvent as MouseEvent;
|
||||
mousePosition = getEventPosition(event.sourceEvent, containerBounds!);
|
||||
|
||||
updateNodes(pointerPos);
|
||||
updateNodes(pointerPos, event.sourceEvent as MouseEvent);
|
||||
}
|
||||
})
|
||||
.on('end', (event: UseDragEvent) => {
|
||||
if (!dragStarted) {
|
||||
if (!dragStarted || abortDrag) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -287,19 +321,25 @@ export function XYDrag({
|
||||
dragStarted = false;
|
||||
cancelAnimationFrame(autoPanId);
|
||||
|
||||
if (dragItems) {
|
||||
const { nodes, updateNodePositions, onNodeDragStop, onSelectionDragStop } = getStoreItems();
|
||||
const onNodeOrSelectionDragStop = nodeId ? onNodeDragStop : wrapSelectionDragFunc(onSelectionDragStop);
|
||||
if (dragItems.size > 0) {
|
||||
const { nodeLookup, updateNodePositions, onNodeDragStop, onSelectionDragStop } = getStoreItems();
|
||||
|
||||
updateNodePositions(dragItems, false, false);
|
||||
updateNodePositions(dragItems, false);
|
||||
|
||||
const [currentNode, currentNodes] = getEventHandlerParams({
|
||||
nodeId,
|
||||
dragItems,
|
||||
nodes,
|
||||
});
|
||||
onDragStop?.(event.sourceEvent as MouseEvent, dragItems, currentNode, currentNodes);
|
||||
onNodeOrSelectionDragStop?.(event.sourceEvent as MouseEvent, currentNode, currentNodes);
|
||||
if (onDragStop || onNodeDragStop || (!nodeId && onSelectionDragStop)) {
|
||||
const [currentNode, currentNodes] = getEventHandlerParams({
|
||||
nodeId,
|
||||
dragItems,
|
||||
nodeLookup,
|
||||
});
|
||||
|
||||
onDragStop?.(event.sourceEvent as MouseEvent, dragItems, currentNode, currentNodes);
|
||||
onNodeDragStop?.(event.sourceEvent as MouseEvent, currentNode, currentNodes);
|
||||
|
||||
if (!nodeId) {
|
||||
onSelectionDragStop?.(event.sourceEvent as MouseEvent, currentNodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter((event: MouseEvent) => {
|
||||
@@ -316,7 +356,7 @@ export function XYDrag({
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
d3Selection.on('.drag', null);
|
||||
d3Selection?.on('.drag', null);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { type NodeDragItem, type XYPosition, NodeBase } from '../types';
|
||||
import { type NodeDragItem, type XYPosition, InternalNodeBase, NodeBase, NodeLookup } from '../types';
|
||||
|
||||
export function wrapSelectionDragFunc(selectionFunc?: (event: MouseEvent, nodes: NodeBase[]) => void) {
|
||||
return (event: MouseEvent, _: NodeBase, nodes: NodeBase[]) => selectionFunc?.(event, nodes);
|
||||
}
|
||||
|
||||
export function isParentSelected<NodeType extends NodeBase>(node: NodeType, nodes: NodeType[]): boolean {
|
||||
if (!node.parentNode) {
|
||||
export function isParentSelected<NodeType extends NodeBase>(node: NodeType, nodeLookup: NodeLookup): boolean {
|
||||
if (!node.parentId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parentNode = nodes.find((node) => node.id === node.parentNode);
|
||||
const parentNode = nodeLookup.get(node.parentId);
|
||||
|
||||
if (!parentNode) {
|
||||
return false;
|
||||
@@ -19,7 +15,7 @@ export function isParentSelected<NodeType extends NodeBase>(node: NodeType, node
|
||||
return true;
|
||||
}
|
||||
|
||||
return isParentSelected(parentNode, nodes);
|
||||
return isParentSelected(parentNode, nodeLookup);
|
||||
}
|
||||
|
||||
export function hasSelector(target: Element, selector: string, domNode: Element): boolean {
|
||||
@@ -36,60 +32,84 @@ export function hasSelector(target: Element, selector: string, domNode: Element)
|
||||
|
||||
// looks for all selected nodes and created a NodeDragItem for each of them
|
||||
export function getDragItems<NodeType extends NodeBase>(
|
||||
nodes: NodeType[],
|
||||
nodeLookup: Map<string, InternalNodeBase<NodeType>>,
|
||||
nodesDraggable: boolean,
|
||||
mousePos: XYPosition,
|
||||
nodeId?: string
|
||||
): NodeDragItem[] {
|
||||
return nodes
|
||||
.filter(
|
||||
(n) =>
|
||||
(n.selected || n.id === nodeId) &&
|
||||
(!n.parentNode || !isParentSelected(n, nodes)) &&
|
||||
(n.draggable || (nodesDraggable && typeof n.draggable === 'undefined'))
|
||||
)
|
||||
.map((n) => ({
|
||||
id: n.id,
|
||||
position: n.position || { x: 0, y: 0 },
|
||||
positionAbsolute: n.positionAbsolute || { x: 0, y: 0 },
|
||||
distance: {
|
||||
x: mousePos.x - (n.positionAbsolute?.x ?? 0),
|
||||
y: mousePos.y - (n.positionAbsolute?.y ?? 0),
|
||||
},
|
||||
delta: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
extent: n.extent,
|
||||
parentNode: n.parentNode,
|
||||
width: n.width,
|
||||
height: n.height,
|
||||
origin: n.origin,
|
||||
expandParent: n.expandParent,
|
||||
}));
|
||||
): Map<string, NodeDragItem> {
|
||||
const dragItems = new Map<string, NodeDragItem>();
|
||||
|
||||
for (const [id, node] of nodeLookup) {
|
||||
if (
|
||||
(node.selected || node.id === nodeId) &&
|
||||
(!node.parentId || !isParentSelected(node, nodeLookup)) &&
|
||||
(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'))
|
||||
) {
|
||||
const internalNode = nodeLookup.get(id);
|
||||
|
||||
if (internalNode) {
|
||||
dragItems.set(id, {
|
||||
id,
|
||||
position: internalNode.position || { x: 0, y: 0 },
|
||||
distance: {
|
||||
x: mousePos.x - internalNode.internals.positionAbsolute.x,
|
||||
y: mousePos.y - internalNode.internals.positionAbsolute.y,
|
||||
},
|
||||
extent: internalNode.extent,
|
||||
parentId: internalNode.parentId,
|
||||
origin: internalNode.origin,
|
||||
expandParent: internalNode.expandParent,
|
||||
internals: {
|
||||
positionAbsolute: internalNode.internals.positionAbsolute || { x: 0, y: 0 },
|
||||
},
|
||||
measured: {
|
||||
width: internalNode.measured.width ?? 0,
|
||||
height: internalNode.measured.height ?? 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dragItems;
|
||||
}
|
||||
|
||||
// returns two params:
|
||||
// 1. the dragged node (or the first of the list, if we are dragging a node selection)
|
||||
// 2. array of selected nodes (for multi selections)
|
||||
export function getEventHandlerParams<NodeType extends NodeBase>({
|
||||
export function getEventHandlerParams<NodeType extends InternalNodeBase>({
|
||||
nodeId,
|
||||
dragItems,
|
||||
nodes,
|
||||
nodeLookup,
|
||||
}: {
|
||||
nodeId?: string;
|
||||
dragItems: NodeDragItem[];
|
||||
nodes: NodeType[];
|
||||
}): [NodeType, NodeType[]] {
|
||||
const extentedDragItems: NodeType[] = dragItems.map((n) => {
|
||||
const node = nodes.find((node) => node.id === n.id)!;
|
||||
dragItems: Map<string, NodeDragItem>;
|
||||
nodeLookup: Map<string, NodeType>;
|
||||
}): [NodeBase, NodeBase[]] {
|
||||
const nodesFromDragItems: NodeBase[] = [];
|
||||
|
||||
return {
|
||||
for (const [id, dragItem] of dragItems) {
|
||||
const node = nodeLookup.get(id)?.internals.userNode;
|
||||
|
||||
if (node) {
|
||||
nodesFromDragItems.push({
|
||||
...node,
|
||||
position: dragItem.position,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!nodeId) {
|
||||
return [nodesFromDragItems[0], nodesFromDragItems];
|
||||
}
|
||||
|
||||
const node = nodeLookup.get(nodeId)!.internals.userNode;
|
||||
|
||||
return [
|
||||
{
|
||||
...node,
|
||||
position: n.position,
|
||||
positionAbsolute: n.positionAbsolute,
|
||||
};
|
||||
});
|
||||
|
||||
return [nodeId ? extentedDragItems.find((n) => n.id === nodeId)! : extentedDragItems[0], extentedDragItems];
|
||||
position: dragItems.get(nodeId)?.position || node.position,
|
||||
},
|
||||
nodesFromDragItems,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,73 +1,25 @@
|
||||
import { pointToRendererPoint, rendererPointToPoint, getHostForElement, calcAutoPan, getEventPosition } from '../utils';
|
||||
import {
|
||||
pointToRendererPoint,
|
||||
getHostForElement,
|
||||
calcAutoPan,
|
||||
getEventPosition,
|
||||
getHandlePosition,
|
||||
rendererPointToPoint,
|
||||
} from '../utils';
|
||||
import {
|
||||
ConnectionMode,
|
||||
type OnConnect,
|
||||
type OnConnectStart,
|
||||
type HandleType,
|
||||
Position,
|
||||
oppositePosition,
|
||||
ConnectionInProgress,
|
||||
type Handle,
|
||||
type Connection,
|
||||
type PanBy,
|
||||
type NodeBase,
|
||||
type Transform,
|
||||
type ConnectingHandle,
|
||||
type OnConnectEnd,
|
||||
type UpdateConnection,
|
||||
type IsValidConnection,
|
||||
type ConnectionHandle,
|
||||
} from '../types';
|
||||
|
||||
import { getClosestHandle, getConnectionStatus, getHandleLookup, getHandleType, resetRecentHandle } from './utils';
|
||||
|
||||
export type OnPointerDownParams = {
|
||||
autoPanOnConnect: boolean;
|
||||
connectionMode: ConnectionMode;
|
||||
connectionRadius: number;
|
||||
domNode: HTMLDivElement | null;
|
||||
handleId: string | null;
|
||||
nodeId: string;
|
||||
isTarget: boolean;
|
||||
nodes: NodeBase[];
|
||||
lib: string;
|
||||
edgeUpdaterType?: HandleType;
|
||||
updateConnection: UpdateConnection;
|
||||
panBy: PanBy;
|
||||
cancelConnection: () => void;
|
||||
onConnectStart?: OnConnectStart;
|
||||
onConnect?: OnConnect;
|
||||
onConnectEnd?: OnConnectEnd;
|
||||
isValidConnection?: IsValidConnection;
|
||||
onEdgeUpdateEnd?: (evt: MouseEvent | TouchEvent) => void;
|
||||
getTransform: () => Transform;
|
||||
};
|
||||
|
||||
export type IsValidParams = {
|
||||
handle: Pick<ConnectionHandle, 'nodeId' | 'id' | 'type'> | null;
|
||||
connectionMode: ConnectionMode;
|
||||
fromNodeId: string;
|
||||
fromHandleId: string | null;
|
||||
fromType: HandleType;
|
||||
isValidConnection?: IsValidConnection;
|
||||
doc: Document | ShadowRoot;
|
||||
lib: string;
|
||||
};
|
||||
|
||||
export type XYHandleInstance = {
|
||||
onPointerDown: (event: MouseEvent | TouchEvent, params: OnPointerDownParams) => void;
|
||||
isValid: (event: MouseEvent | TouchEvent, params: IsValidParams) => Result;
|
||||
};
|
||||
|
||||
type Result = {
|
||||
handleDomNode: Element | null;
|
||||
isValid: boolean;
|
||||
connection: Connection;
|
||||
endHandle: ConnectingHandle | null;
|
||||
};
|
||||
|
||||
const nullConnection: Connection = { source: null, target: null, sourceHandle: null, targetHandle: null };
|
||||
import { getClosestHandle, isConnectionValid, getHandleLookup, getHandleType } from './utils';
|
||||
import { IsValidParams, OnPointerDownParams, Result, XYHandleInstance } from './types';
|
||||
|
||||
const alwaysValid = () => true;
|
||||
|
||||
let connectionStartHandle: ConnectingHandle | null = null;
|
||||
|
||||
function onPointerDown(
|
||||
event: MouseEvent | TouchEvent,
|
||||
{
|
||||
@@ -78,24 +30,27 @@ function onPointerDown(
|
||||
edgeUpdaterType,
|
||||
isTarget,
|
||||
domNode,
|
||||
nodes,
|
||||
nodeLookup,
|
||||
lib,
|
||||
autoPanOnConnect,
|
||||
flowId,
|
||||
panBy,
|
||||
cancelConnection,
|
||||
onConnectStart,
|
||||
onConnect,
|
||||
onConnectEnd,
|
||||
isValidConnection = alwaysValid,
|
||||
onEdgeUpdateEnd,
|
||||
onReconnectEnd,
|
||||
updateConnection,
|
||||
getTransform,
|
||||
getFromHandle,
|
||||
autoPanSpeed,
|
||||
}: OnPointerDownParams
|
||||
) {
|
||||
// when xyflow is used inside a shadow root we can't use document
|
||||
const doc = getHostForElement(event.target as HTMLElement);
|
||||
let autoPanId = 0;
|
||||
let closestHandle: ConnectionHandle | null;
|
||||
let closestHandle: Handle | null;
|
||||
|
||||
const { x, y } = getEventPosition(event);
|
||||
const clickedHandle = doc?.elementFromPoint(x, y);
|
||||
@@ -106,15 +61,14 @@ function onPointerDown(
|
||||
return;
|
||||
}
|
||||
|
||||
let prevActiveHandle: Element;
|
||||
let connectionPosition = getEventPosition(event, containerBounds);
|
||||
let position = getEventPosition(event, containerBounds);
|
||||
let autoPanStarted = false;
|
||||
let connection: Connection | null = null;
|
||||
let isValid = false;
|
||||
let isValid: boolean | null = false;
|
||||
let handleDomNode: Element | null = null;
|
||||
|
||||
const handleLookup = getHandleLookup({
|
||||
nodes,
|
||||
const [handleLookup, fromHandleInternal] = getHandleLookup({
|
||||
nodeLookup,
|
||||
nodeId,
|
||||
handleId,
|
||||
handleType,
|
||||
@@ -125,34 +79,54 @@ function onPointerDown(
|
||||
if (!autoPanOnConnect || !containerBounds) {
|
||||
return;
|
||||
}
|
||||
const [x, y] = calcAutoPan(connectionPosition, containerBounds);
|
||||
const [x, y] = calcAutoPan(position, containerBounds, autoPanSpeed);
|
||||
|
||||
panBy({ x, y });
|
||||
autoPanId = requestAnimationFrame(autoPan);
|
||||
}
|
||||
|
||||
// Stays the same for all consecutive pointermove events
|
||||
connectionStartHandle = {
|
||||
const fromHandle: Handle = {
|
||||
...fromHandleInternal,
|
||||
nodeId,
|
||||
handleId,
|
||||
type: handleType,
|
||||
position: fromHandleInternal.position,
|
||||
};
|
||||
|
||||
updateConnection({
|
||||
connectionPosition,
|
||||
connectionStatus: null,
|
||||
// connectionNodeId etc will be removed in the next major in favor of connectionStartHandle
|
||||
connectionStartHandle,
|
||||
connectionEndHandle: null,
|
||||
});
|
||||
const fromNodeInternal = nodeLookup.get(nodeId)!;
|
||||
|
||||
const from = getHandlePosition(fromNodeInternal, fromHandle, Position.Left, true);
|
||||
|
||||
const newConnection: ConnectionInProgress = {
|
||||
inProgress: true,
|
||||
isValid: null,
|
||||
|
||||
from,
|
||||
fromHandle,
|
||||
fromPosition: fromHandle.position,
|
||||
fromNode: fromNodeInternal.internals.userNode,
|
||||
|
||||
to: position,
|
||||
toHandle: null,
|
||||
toPosition: oppositePosition[fromHandle.position],
|
||||
toNode: null,
|
||||
};
|
||||
|
||||
updateConnection(newConnection);
|
||||
let previousConnection: ConnectionInProgress = newConnection;
|
||||
|
||||
onConnectStart?.(event, { nodeId, handleId, handleType });
|
||||
|
||||
function onPointerMove(event: MouseEvent | TouchEvent) {
|
||||
if (!getFromHandle() || !fromHandle) {
|
||||
onPointerUp(event);
|
||||
return;
|
||||
}
|
||||
|
||||
const transform = getTransform();
|
||||
connectionPosition = getEventPosition(event, containerBounds);
|
||||
position = getEventPosition(event, containerBounds);
|
||||
closestHandle = getClosestHandle(
|
||||
pointToRendererPoint(connectionPosition, transform, false, [1, 1]),
|
||||
pointToRendererPoint(position, transform, false, [1, 1]),
|
||||
connectionRadius,
|
||||
handleLookup
|
||||
);
|
||||
@@ -171,39 +145,43 @@ function onPointerDown(
|
||||
isValidConnection,
|
||||
doc,
|
||||
lib,
|
||||
flowId,
|
||||
handleLookup,
|
||||
});
|
||||
|
||||
handleDomNode = result.handleDomNode;
|
||||
connection = result.connection;
|
||||
isValid = result.isValid;
|
||||
isValid = isConnectionValid(!!closestHandle, result.isValid);
|
||||
|
||||
updateConnection({
|
||||
connectionStartHandle,
|
||||
connectionPosition:
|
||||
const newConnection: ConnectionInProgress = {
|
||||
// from stays the same
|
||||
...previousConnection,
|
||||
isValid,
|
||||
to:
|
||||
closestHandle && isValid
|
||||
? rendererPointToPoint(
|
||||
{
|
||||
x: closestHandle.x,
|
||||
y: closestHandle.y,
|
||||
},
|
||||
transform
|
||||
)
|
||||
: connectionPosition,
|
||||
connectionStatus: getConnectionStatus(!!closestHandle, isValid),
|
||||
connectionEndHandle: result.endHandle,
|
||||
});
|
||||
? rendererPointToPoint({ x: closestHandle.x, y: closestHandle.y }, transform)
|
||||
: position,
|
||||
toHandle: result.toHandle,
|
||||
toPosition: isValid && result.toHandle ? result.toHandle.position : oppositePosition[fromHandle.position],
|
||||
toNode: result.toHandle ? nodeLookup.get(result.toHandle.nodeId)!.internals.userNode : null,
|
||||
};
|
||||
|
||||
if (!closestHandle && !isValid && !handleDomNode) {
|
||||
return resetRecentHandle(prevActiveHandle, lib);
|
||||
// we don't want to trigger an update when the connection
|
||||
// is snapped to the same handle as before
|
||||
if (
|
||||
isValid &&
|
||||
closestHandle &&
|
||||
previousConnection.toHandle &&
|
||||
newConnection.toHandle &&
|
||||
previousConnection.toHandle.type === newConnection.toHandle.type &&
|
||||
previousConnection.toHandle.nodeId === newConnection.toHandle.nodeId &&
|
||||
previousConnection.toHandle.id === newConnection.toHandle.id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (connection.source !== connection.target && handleDomNode) {
|
||||
resetRecentHandle(prevActiveHandle, lib);
|
||||
prevActiveHandle = handleDomNode;
|
||||
handleDomNode.classList.add('connecting', `${lib}-flow__handle-connecting`);
|
||||
handleDomNode.classList.toggle('valid', isValid);
|
||||
handleDomNode.classList.toggle(`${lib}-flow__handle-valid`, isValid);
|
||||
}
|
||||
updateConnection(newConnection);
|
||||
previousConnection = newConnection;
|
||||
}
|
||||
|
||||
function onPointerUp(event: MouseEvent | TouchEvent) {
|
||||
@@ -216,17 +194,15 @@ function onPointerDown(
|
||||
onConnectEnd?.(event);
|
||||
|
||||
if (edgeUpdaterType) {
|
||||
onEdgeUpdateEnd?.(event);
|
||||
onReconnectEnd?.(event);
|
||||
}
|
||||
|
||||
resetRecentHandle(prevActiveHandle, lib);
|
||||
cancelConnection();
|
||||
cancelAnimationFrame(autoPanId);
|
||||
autoPanStarted = false;
|
||||
isValid = false;
|
||||
connection = null;
|
||||
handleDomNode = null;
|
||||
connectionStartHandle = null;
|
||||
|
||||
doc.removeEventListener('mousemove', onPointerMove as EventListener);
|
||||
doc.removeEventListener('mouseup', onPointerUp as EventListener);
|
||||
@@ -253,13 +229,16 @@ function isValidHandle(
|
||||
fromType,
|
||||
doc,
|
||||
lib,
|
||||
flowId,
|
||||
isValidConnection = alwaysValid,
|
||||
handleLookup,
|
||||
}: IsValidParams
|
||||
) {
|
||||
const isTarget = fromType === 'target';
|
||||
const handleDomNode = doc.querySelector(
|
||||
`.${lib}-flow__handle[data-id="${handle?.nodeId}-${handle?.id}-${handle?.type}"]`
|
||||
);
|
||||
const handleDomNode = handle
|
||||
? doc.querySelector(`.${lib}-flow__handle[data-id="${flowId}-${handle?.nodeId}-${handle?.id}-${handle?.type}"]`)
|
||||
: null;
|
||||
|
||||
const { x, y } = getEventPosition(event);
|
||||
const handleBelow = doc.elementFromPoint(x, y);
|
||||
// we always want to prioritize the handle below the mouse cursor over the closest distance handle,
|
||||
@@ -269,8 +248,8 @@ function isValidHandle(
|
||||
const result: Result = {
|
||||
handleDomNode: handleToCheck,
|
||||
isValid: false,
|
||||
connection: nullConnection,
|
||||
endHandle: null,
|
||||
connection: null,
|
||||
toHandle: null,
|
||||
};
|
||||
|
||||
if (handleToCheck) {
|
||||
@@ -280,6 +259,10 @@ function isValidHandle(
|
||||
const connectable = handleToCheck.classList.contains('connectable');
|
||||
const connectableEnd = handleToCheck.classList.contains('connectableend');
|
||||
|
||||
if (!handleNodeId) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const connection: Connection = {
|
||||
source: isTarget ? handleNodeId : fromNodeId,
|
||||
sourceHandle: isTarget ? handleId : fromHandleId,
|
||||
@@ -297,14 +280,18 @@ function isValidHandle(
|
||||
? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target')
|
||||
: handleNodeId !== fromNodeId || handleId !== fromHandleId);
|
||||
|
||||
if (isValid) {
|
||||
result.endHandle = {
|
||||
nodeId: handleNodeId as string,
|
||||
handleId,
|
||||
type: handleType as HandleType,
|
||||
};
|
||||
result.isValid = isValid && isValidConnection(connection);
|
||||
|
||||
result.isValid = isValidConnection(connection);
|
||||
if (handleLookup) {
|
||||
const toHandle = handleLookup.find(
|
||||
(h) => h.id === handleId && h.nodeId === handleNodeId && h.type === handleType
|
||||
);
|
||||
|
||||
if (toHandle) {
|
||||
result.toHandle = {
|
||||
...toHandle,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
ConnectionMode,
|
||||
type Connection,
|
||||
type OnConnect,
|
||||
type OnConnectStart,
|
||||
type HandleType,
|
||||
type PanBy,
|
||||
type Transform,
|
||||
type Handle,
|
||||
type OnConnectEnd,
|
||||
type UpdateConnection,
|
||||
type IsValidConnection,
|
||||
NodeLookup,
|
||||
} from '../types';
|
||||
|
||||
export type OnPointerDownParams = {
|
||||
autoPanOnConnect: boolean;
|
||||
connectionMode: ConnectionMode;
|
||||
connectionRadius: number;
|
||||
domNode: HTMLDivElement | null;
|
||||
handleId: string | null;
|
||||
nodeId: string;
|
||||
isTarget: boolean;
|
||||
nodeLookup: NodeLookup;
|
||||
lib: string;
|
||||
flowId: string | null;
|
||||
edgeUpdaterType?: HandleType;
|
||||
updateConnection: UpdateConnection;
|
||||
panBy: PanBy;
|
||||
cancelConnection: () => void;
|
||||
onConnectStart?: OnConnectStart;
|
||||
onConnect?: OnConnect;
|
||||
onConnectEnd?: OnConnectEnd;
|
||||
isValidConnection?: IsValidConnection;
|
||||
onReconnectEnd?: (evt: MouseEvent | TouchEvent) => void;
|
||||
getTransform: () => Transform;
|
||||
getFromHandle: () => Handle | null;
|
||||
autoPanSpeed?: number;
|
||||
};
|
||||
|
||||
export type IsValidParams = {
|
||||
handle: Pick<Handle, 'nodeId' | 'id' | 'type'> | null;
|
||||
connectionMode: ConnectionMode;
|
||||
fromNodeId: string;
|
||||
fromHandleId: string | null;
|
||||
fromType: HandleType;
|
||||
isValidConnection?: IsValidConnection;
|
||||
doc: Document | ShadowRoot;
|
||||
lib: string;
|
||||
flowId: string | null;
|
||||
handleLookup?: Handle[];
|
||||
};
|
||||
|
||||
export type XYHandleInstance = {
|
||||
onPointerDown: (event: MouseEvent | TouchEvent, params: OnPointerDownParams) => void;
|
||||
isValid: (event: MouseEvent | TouchEvent, params: IsValidParams) => Result;
|
||||
};
|
||||
|
||||
export type Result = {
|
||||
handleDomNode: Element | null;
|
||||
isValid: boolean;
|
||||
connection: Connection | null;
|
||||
toHandle: Handle | null;
|
||||
};
|
||||
@@ -1,44 +1,39 @@
|
||||
import { getHandlePosition } from '../utils';
|
||||
import {
|
||||
ConnectionStatus,
|
||||
type HandleType,
|
||||
type NodeHandleBounds,
|
||||
type XYPosition,
|
||||
type NodeBase,
|
||||
type ConnectionHandle,
|
||||
type Handle,
|
||||
InternalNodeBase,
|
||||
NodeLookup,
|
||||
} from '../types';
|
||||
import { internalsSymbol } from '../constants';
|
||||
|
||||
// this functions collects all handles and adds an absolute position
|
||||
// so that we can later find the closest handle to the mouse position
|
||||
export function getHandles(
|
||||
node: NodeBase,
|
||||
function getHandles(
|
||||
node: InternalNodeBase,
|
||||
handleBounds: NodeHandleBounds,
|
||||
type: HandleType,
|
||||
currentHandle: string
|
||||
): ConnectionHandle[] {
|
||||
return (handleBounds[type] || []).reduce<ConnectionHandle[]>((res, h) => {
|
||||
if (`${node.id}-${h.id}-${type}` !== currentHandle) {
|
||||
res.push({
|
||||
id: h.id || null,
|
||||
type,
|
||||
nodeId: node.id,
|
||||
x: (node.positionAbsolute?.x ?? 0) + h.x + h.width / 2,
|
||||
y: (node.positionAbsolute?.y ?? 0) + h.y + h.height / 2,
|
||||
});
|
||||
currentHandle: { nodeId: string; handleId: string | null; handleType: HandleType }
|
||||
): [Handle[], Handle | null] {
|
||||
let excludedHandle = null;
|
||||
const handles = (handleBounds[type] || []).reduce<Handle[]>((res, handle) => {
|
||||
if (node.id === currentHandle.nodeId && type === currentHandle.handleType && handle.id === currentHandle.handleId) {
|
||||
excludedHandle = handle;
|
||||
} else {
|
||||
const handleXY = getHandlePosition(node, handle, handle.position, true);
|
||||
res.push({ ...handle, ...handleXY });
|
||||
}
|
||||
return res;
|
||||
}, []);
|
||||
return [handles, excludedHandle];
|
||||
}
|
||||
|
||||
export function getClosestHandle(
|
||||
pos: XYPosition,
|
||||
connectionRadius: number,
|
||||
handles: ConnectionHandle[]
|
||||
): ConnectionHandle | null {
|
||||
let closestHandles: ConnectionHandle[] = [];
|
||||
export function getClosestHandle(pos: XYPosition, connectionRadius: number, handles: Handle[]): Handle | null {
|
||||
let closestHandles: Handle[] = [];
|
||||
let minDistance = Infinity;
|
||||
|
||||
handles.forEach((handle) => {
|
||||
for (const handle of handles) {
|
||||
const distance = Math.sqrt(Math.pow(handle.x - pos.x, 2) + Math.pow(handle.y - pos.y, 2));
|
||||
if (distance <= connectionRadius) {
|
||||
if (distance < minDistance) {
|
||||
@@ -49,7 +44,7 @@ export function getClosestHandle(
|
||||
}
|
||||
minDistance = distance;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!closestHandles.length) {
|
||||
return null;
|
||||
@@ -62,28 +57,32 @@ export function getClosestHandle(
|
||||
}
|
||||
|
||||
type GetHandleLookupParams = {
|
||||
nodes: NodeBase[];
|
||||
nodeLookup: NodeLookup;
|
||||
nodeId: string;
|
||||
handleId: string | null;
|
||||
handleType: string;
|
||||
handleType: HandleType;
|
||||
};
|
||||
|
||||
export function getHandleLookup({ nodes, nodeId, handleId, handleType }: GetHandleLookupParams) {
|
||||
return nodes.reduce<ConnectionHandle[]>((res, node) => {
|
||||
if (node[internalsSymbol]) {
|
||||
const { handleBounds } = node[internalsSymbol];
|
||||
let sourceHandles: ConnectionHandle[] = [];
|
||||
let targetHandles: ConnectionHandle[] = [];
|
||||
export function getHandleLookup({
|
||||
nodeLookup,
|
||||
nodeId,
|
||||
handleId,
|
||||
handleType,
|
||||
}: GetHandleLookupParams): [Handle[], Handle] {
|
||||
const connectionHandles: Handle[] = [];
|
||||
const currentHandle = { nodeId, handleId, handleType };
|
||||
let excludedHandle: Handle | null = null;
|
||||
|
||||
if (handleBounds) {
|
||||
sourceHandles = getHandles(node, handleBounds, 'source', `${nodeId}-${handleId}-${handleType}`);
|
||||
targetHandles = getHandles(node, handleBounds, 'target', `${nodeId}-${handleId}-${handleType}`);
|
||||
}
|
||||
|
||||
res.push(...sourceHandles, ...targetHandles);
|
||||
for (const node of nodeLookup.values()) {
|
||||
if (node.internals.handleBounds) {
|
||||
const [sourceHandles, excludedSource] = getHandles(node, node.internals.handleBounds, 'source', currentHandle);
|
||||
const [targetHandles, excludedTarget] = getHandles(node, node.internals.handleBounds, 'target', currentHandle);
|
||||
excludedHandle = excludedHandle ? excludedHandle : excludedSource ?? excludedTarget;
|
||||
connectionHandles.push(...sourceHandles, ...targetHandles);
|
||||
}
|
||||
return res;
|
||||
}, []);
|
||||
}
|
||||
|
||||
return [connectionHandles, excludedHandle!];
|
||||
}
|
||||
|
||||
export function getHandleType(
|
||||
@@ -101,18 +100,14 @@ export function getHandleType(
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resetRecentHandle(handleDomNode: Element, lib: string): void {
|
||||
handleDomNode?.classList.remove('valid', 'connecting', `${lib}-flow__handle-valid`, `${lib}-flow__handle-connecting`);
|
||||
}
|
||||
|
||||
export function getConnectionStatus(isInsideConnectionRadius: boolean, isHandleValid: boolean) {
|
||||
let connectionStatus = null;
|
||||
export function isConnectionValid(isInsideConnectionRadius: boolean, isHandleValid: boolean) {
|
||||
let isValid: boolean | null = null;
|
||||
|
||||
if (isHandleValid) {
|
||||
connectionStatus = 'valid';
|
||||
isValid = true;
|
||||
} else if (isInsideConnectionRadius && !isHandleValid) {
|
||||
connectionStatus = 'invalid';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return connectionStatus as ConnectionStatus;
|
||||
return isValid;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ export function XYMinimap({ domNode, panZoom, getTransform, getViewScale }: XYMi
|
||||
zoomable = true,
|
||||
inversePan = false,
|
||||
}: XYMinimapUpdate) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const zoomHandler = (event: D3ZoomEvent<SVGSVGElement, any>) => {
|
||||
const transform = getTransform();
|
||||
|
||||
@@ -54,18 +55,36 @@ export function XYMinimap({ domNode, panZoom, getTransform, getViewScale }: XYMi
|
||||
panZoom.scaleTo(nextZoom);
|
||||
};
|
||||
|
||||
let panStart = [0, 0];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const panStartHandler = (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
if (event.sourceEvent.type === 'mousedown' || event.sourceEvent.type === 'touchstart') {
|
||||
panStart = [
|
||||
event.sourceEvent.clientX ?? event.sourceEvent.touches[0].clientX,
|
||||
event.sourceEvent.clientY ?? event.sourceEvent.touches[0].clientY,
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const panHandler = (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
const transform = getTransform();
|
||||
|
||||
if (event.sourceEvent.type !== 'mousemove' || !panZoom) {
|
||||
if ((event.sourceEvent.type !== 'mousemove' && event.sourceEvent.type !== 'touchmove') || !panZoom) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @TODO: how to calculate the correct next position? Math.max(1, transform[2]) is a workaround.
|
||||
const moveScale = getViewScale() * Math.max(1, transform[2]) * (inversePan ? -1 : 1);
|
||||
const panCurrent = [
|
||||
event.sourceEvent.clientX ?? event.sourceEvent.touches[0].clientX,
|
||||
event.sourceEvent.clientY ?? event.sourceEvent.touches[0].clientY,
|
||||
];
|
||||
const panDelta = [panCurrent[0] - panStart[0], panCurrent[1] - panStart[1]];
|
||||
panStart = panCurrent;
|
||||
|
||||
const moveScale = getViewScale() * Math.max(transform[2], Math.log(transform[2])) * (inversePan ? -1 : 1);
|
||||
const position = {
|
||||
x: transform[0] - event.sourceEvent.movementX * moveScale,
|
||||
y: transform[1] - event.sourceEvent.movementY * moveScale,
|
||||
x: transform[0] - panDelta[0] * moveScale,
|
||||
y: transform[1] - panDelta[1] * moveScale,
|
||||
};
|
||||
const extent: CoordinateExtent = [
|
||||
[0, 0],
|
||||
@@ -84,8 +103,11 @@ export function XYMinimap({ domNode, panZoom, getTransform, getViewScale }: XYMi
|
||||
};
|
||||
|
||||
const zoomAndPanHandler = zoom()
|
||||
.on('start', panStartHandler)
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
.on('zoom', pannable ? panHandler : null)
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
.on('zoom.wheel', zoomable ? zoomHandler : null);
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ export function XYPanZoom({
|
||||
);
|
||||
|
||||
const d3ZoomHandler = d3Selection.on('wheel.zoom')!;
|
||||
const d3DblClickZoomHandler = d3Selection.on('dblclick.zoom')!;
|
||||
d3ZoomInstance.wheelDelta(wheelDelta);
|
||||
|
||||
function setTransform(transform: ZoomTransform, options?: PanZoomTransformOptions) {
|
||||
@@ -172,6 +173,15 @@ export function XYPanZoom({
|
||||
lib,
|
||||
});
|
||||
d3ZoomInstance.filter(filter);
|
||||
|
||||
// We cannot add zoomOnDoubleClick to the filter above because
|
||||
// double tapping on touch screens circumvents the filter and
|
||||
// dblclick.zoom is fired on the selection directly
|
||||
if (zoomOnDoubleClick) {
|
||||
d3Selection.on('dblclick.zoom', d3DblClickZoomHandler);
|
||||
} else {
|
||||
d3Selection.on('dblclick.zoom', null);
|
||||
}
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
@@ -211,6 +221,7 @@ export function XYPanZoom({
|
||||
currentTransform.x !== viewport.x ||
|
||||
currentTransform.y !== viewport.y
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
d3ZoomInstance?.transform(d3Selection, nextTransform, null, { sync: true });
|
||||
}
|
||||
|
||||
@@ -78,10 +78,9 @@ export function createPanOnScrollHandler({
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
const currentZoom = d3Selection.property('__zoom').k || 1;
|
||||
const _isMacOs = isMacOs();
|
||||
|
||||
// macos sets ctrlKey=true for pinch gesture on a trackpad
|
||||
if (event.ctrlKey && zoomOnPinch && _isMacOs) {
|
||||
if (event.ctrlKey && zoomOnPinch) {
|
||||
const point = pointer(event);
|
||||
const pinchDelta = wheelDelta(event);
|
||||
const zoom = currentZoom * Math.pow(2, pinchDelta);
|
||||
@@ -98,7 +97,7 @@ export function createPanOnScrollHandler({
|
||||
let deltaY = panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize;
|
||||
|
||||
// this enables vertical scrolling with shift + scroll on windows
|
||||
if (!_isMacOs && event.shiftKey && panOnScrollMode !== PanOnScrollMode.Vertical) {
|
||||
if (!isMacOs() && event.shiftKey && panOnScrollMode !== PanOnScrollMode.Vertical) {
|
||||
deltaX = event.deltaY * deltaNormalize;
|
||||
deltaY = 0;
|
||||
}
|
||||
@@ -138,7 +137,10 @@ export function createPanOnScrollHandler({
|
||||
|
||||
export function createZoomOnScrollHandler({ noWheelClassName, preventScrolling, d3ZoomHandler }: ZoomOnScrollParams) {
|
||||
return function (this: Element, event: any, d: unknown) {
|
||||
if (!preventScrolling || isWrappedWithClass(event, noWheelClassName)) {
|
||||
// we still want to enable pinch zooming even if preventScrolling is set to false
|
||||
const preventZoom = !preventScrolling && event.type === 'wheel' && !event.ctrlKey;
|
||||
|
||||
if (preventZoom || isWrappedWithClass(event, noWheelClassName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -205,6 +207,7 @@ export function createPanZoomEndHandler({
|
||||
if (event.sourceEvent?.internal) {
|
||||
return;
|
||||
}
|
||||
|
||||
zoomPanValues.isZoomingOrPanning = false;
|
||||
|
||||
if (
|
||||
|
||||
@@ -48,11 +48,6 @@ export function createFilter({
|
||||
return false;
|
||||
}
|
||||
|
||||
// if zoom on double click is disabled, we prevent the double click event
|
||||
if (!zoomOnDoubleClick && event.type === 'dblclick') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if the target element is inside an element with the nowheel class, we prevent zooming
|
||||
if (isWrappedWithClass(event, noWheelClassName) && event.type === 'wheel') {
|
||||
return false;
|
||||
@@ -61,7 +56,7 @@ export function createFilter({
|
||||
// if the target element is inside an element with the nopan class, we prevent panning
|
||||
if (
|
||||
isWrappedWithClass(event, noPanClassName) &&
|
||||
((!panOnScroll && event.type !== 'wheel') || (panOnScroll && event.type === 'wheel'))
|
||||
(event.type !== 'wheel' || (panOnScroll && event.type === 'wheel' && !zoomActivationKeyPressed))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -70,6 +65,11 @@ export function createFilter({
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!zoomOnPinch && event.type === 'touchstart' && event.touches?.length > 1) {
|
||||
event.preventDefault(); // if you manage to start with 2 touches, we prevent native zoom
|
||||
return false;
|
||||
}
|
||||
|
||||
// when there is no scroll handling enabled, we prevent all wheel events
|
||||
if (!zoomScroll && !panOnScroll && !pinchZoom && event.type === 'wheel') {
|
||||
return false;
|
||||
@@ -81,11 +81,7 @@ export function createFilter({
|
||||
}
|
||||
|
||||
// if the pane is only movable using allowed clicks
|
||||
if (
|
||||
Array.isArray(panOnDrag) &&
|
||||
!panOnDrag.includes(event.button) &&
|
||||
(event.type === 'mousedown' || event.type === 'touchstart')
|
||||
) {
|
||||
if (Array.isArray(panOnDrag) && !panOnDrag.includes(event.button) && event.type === 'mousedown') {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { type ZoomTransform, zoomIdentity } from 'd3-zoom';
|
||||
|
||||
import { type D3SelectionInstance, type Viewport } from '../types';
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
import { drag } from 'd3-drag';
|
||||
import { select } from 'd3-selection';
|
||||
|
||||
import { getControlDirection, getDimensionsAfterResize, getResizeDirection } from './utils';
|
||||
import { getPointerPosition } from '../utils';
|
||||
import type {
|
||||
CoordinateExtent,
|
||||
InternalNodeBase,
|
||||
NodeBase,
|
||||
NodeLookup,
|
||||
NodeOrigin,
|
||||
Transform,
|
||||
XYPosition,
|
||||
} from '../types';
|
||||
import type { OnResize, OnResizeEnd, OnResizeStart, ResizeDragEvent, ShouldResize, ControlPosition } from './types';
|
||||
|
||||
const initPrevValues = { width: 0, height: 0, x: 0, y: 0 };
|
||||
|
||||
const initStartValues = {
|
||||
...initPrevValues,
|
||||
pointerX: 0,
|
||||
pointerY: 0,
|
||||
aspectRatio: 1,
|
||||
};
|
||||
|
||||
export type XYResizerChange = {
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
export type XYResizerChildChange = {
|
||||
id: string;
|
||||
position: XYPosition;
|
||||
extent?: 'parent' | CoordinateExtent;
|
||||
};
|
||||
|
||||
type XYResizerParams = {
|
||||
domNode: HTMLDivElement;
|
||||
nodeId: string;
|
||||
getStoreItems: () => {
|
||||
nodeLookup: NodeLookup;
|
||||
transform: Transform;
|
||||
snapGrid?: [number, number];
|
||||
snapToGrid: boolean;
|
||||
nodeOrigin: NodeOrigin;
|
||||
};
|
||||
onChange: (changes: XYResizerChange, childChanges: XYResizerChildChange[]) => void;
|
||||
onEnd?: () => void;
|
||||
};
|
||||
|
||||
type XYResizerUpdateParams = {
|
||||
controlPosition: ControlPosition;
|
||||
boundaries: {
|
||||
minWidth: number;
|
||||
minHeight: number;
|
||||
maxWidth: number;
|
||||
maxHeight: number;
|
||||
};
|
||||
keepAspectRatio: boolean;
|
||||
onResizeStart: OnResizeStart | undefined;
|
||||
onResize: OnResize | undefined;
|
||||
onResizeEnd: OnResizeEnd | undefined;
|
||||
shouldResize: ShouldResize | undefined;
|
||||
};
|
||||
|
||||
export type XYResizerInstance = {
|
||||
update: (params: XYResizerUpdateParams) => void;
|
||||
destroy: () => void;
|
||||
};
|
||||
|
||||
function nodeToParentExtent(node: NodeBase): CoordinateExtent {
|
||||
return [
|
||||
[0, 0],
|
||||
[node.measured!.width!, node.measured!.height!],
|
||||
];
|
||||
}
|
||||
|
||||
function nodeToChildExtent(child: NodeBase, parent: NodeBase, nodeOrigin: NodeOrigin): CoordinateExtent {
|
||||
const x = parent.position.x + child.position.x;
|
||||
const y = parent.position.y + child.position.y;
|
||||
const width = child.measured!.width! ?? 0;
|
||||
const height = child.measured!.height! ?? 0;
|
||||
const originOffsetX = nodeOrigin[0] * width;
|
||||
const originOffsetY = nodeOrigin[1] * height;
|
||||
|
||||
return [
|
||||
[x - originOffsetX, y - originOffsetY],
|
||||
[x + width - originOffsetX, y + height - originOffsetY],
|
||||
];
|
||||
}
|
||||
|
||||
export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: XYResizerParams): XYResizerInstance {
|
||||
const selection = select(domNode);
|
||||
|
||||
function update({
|
||||
controlPosition,
|
||||
boundaries,
|
||||
keepAspectRatio,
|
||||
onResizeStart,
|
||||
onResize,
|
||||
onResizeEnd,
|
||||
shouldResize,
|
||||
}: XYResizerUpdateParams) {
|
||||
let prevValues = { ...initPrevValues };
|
||||
let startValues = { ...initStartValues };
|
||||
|
||||
const controlDirection = getControlDirection(controlPosition);
|
||||
|
||||
let node: InternalNodeBase | undefined = undefined;
|
||||
let childNodes: XYResizerChildChange[] = [];
|
||||
let parentNode: InternalNodeBase | undefined = undefined; // Needed to fix expandParent
|
||||
let parentExtent: CoordinateExtent | undefined = undefined;
|
||||
let childExtent: CoordinateExtent | undefined = undefined;
|
||||
|
||||
const dragHandler = drag<HTMLDivElement, unknown>()
|
||||
.on('start', (event: ResizeDragEvent) => {
|
||||
const { nodeLookup, transform, snapGrid, snapToGrid, nodeOrigin } = getStoreItems();
|
||||
node = nodeLookup.get(nodeId);
|
||||
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
|
||||
|
||||
prevValues = {
|
||||
width: node.measured.width ?? 0,
|
||||
height: node.measured.height ?? 0,
|
||||
x: node.position.x ?? 0,
|
||||
y: node.position.y ?? 0,
|
||||
};
|
||||
|
||||
startValues = {
|
||||
...prevValues,
|
||||
pointerX: xSnapped,
|
||||
pointerY: ySnapped,
|
||||
aspectRatio: prevValues.width / prevValues.height,
|
||||
};
|
||||
|
||||
parentNode = undefined;
|
||||
|
||||
if (node.parentId && (node.extent === 'parent' || node.expandParent)) {
|
||||
parentNode = nodeLookup.get(node.parentId);
|
||||
parentExtent = parentNode && node.extent === 'parent' ? nodeToParentExtent(parentNode) : undefined;
|
||||
}
|
||||
|
||||
// Collect all child nodes to correct their relative positions when top/left changes
|
||||
// Determine largest minimal extent the parent node is allowed to resize to
|
||||
childNodes = [];
|
||||
childExtent = undefined;
|
||||
|
||||
for (const [childId, child] of nodeLookup) {
|
||||
if (child.parentId === nodeId) {
|
||||
childNodes.push({
|
||||
id: childId,
|
||||
position: { ...child.position },
|
||||
extent: child.extent,
|
||||
});
|
||||
|
||||
if (child.extent === 'parent' || child.expandParent) {
|
||||
const extent = nodeToChildExtent(child, node, child.origin ?? nodeOrigin);
|
||||
|
||||
if (childExtent) {
|
||||
childExtent = [
|
||||
[Math.min(extent[0][0], childExtent[0][0]), Math.min(extent[0][1], childExtent[0][1])],
|
||||
[Math.max(extent[1][0], childExtent[1][0]), Math.max(extent[1][1], childExtent[1][1])],
|
||||
];
|
||||
} else {
|
||||
childExtent = extent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onResizeStart?.(event, { ...prevValues });
|
||||
})
|
||||
.on('drag', (event: ResizeDragEvent) => {
|
||||
const { transform, snapGrid, snapToGrid, nodeOrigin: storeNodeOrigin } = getStoreItems();
|
||||
const pointerPosition = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
|
||||
const childChanges: XYResizerChildChange[] = [];
|
||||
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const { x: prevX, y: prevY, width: prevWidth, height: prevHeight } = prevValues;
|
||||
const change: XYResizerChange = {};
|
||||
const nodeOrigin = node.origin ?? storeNodeOrigin;
|
||||
|
||||
const { width, height, x, y } = getDimensionsAfterResize(
|
||||
startValues,
|
||||
controlDirection,
|
||||
pointerPosition,
|
||||
boundaries,
|
||||
keepAspectRatio,
|
||||
nodeOrigin,
|
||||
parentExtent,
|
||||
childExtent
|
||||
);
|
||||
|
||||
const isWidthChange = width !== prevWidth;
|
||||
const isHeightChange = height !== prevHeight;
|
||||
|
||||
const isXPosChange = x !== prevX && isWidthChange;
|
||||
const isYPosChange = y !== prevY && isHeightChange;
|
||||
|
||||
if (!isXPosChange && !isYPosChange && !isWidthChange && !isHeightChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isXPosChange || isYPosChange || nodeOrigin[0] === 1 || nodeOrigin[1] === 1) {
|
||||
change.x = isXPosChange ? x : prevValues.x;
|
||||
change.y = isYPosChange ? y : prevValues.y;
|
||||
|
||||
prevValues.x = change.x;
|
||||
prevValues.y = change.y;
|
||||
|
||||
// when top/left changes, correct the relative positions of child nodes
|
||||
// so that they stay in the same position
|
||||
if (childNodes.length > 0) {
|
||||
const xChange = x - prevX;
|
||||
const yChange = y - prevY;
|
||||
|
||||
for (const childNode of childNodes) {
|
||||
childNode.position = {
|
||||
x: childNode.position.x - xChange + nodeOrigin[0] * (width - prevWidth),
|
||||
y: childNode.position.y - yChange + nodeOrigin[1] * (height - prevHeight),
|
||||
};
|
||||
childChanges.push(childNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isWidthChange || isHeightChange) {
|
||||
change.width = isWidthChange ? width : prevValues.width;
|
||||
change.height = isHeightChange ? height : prevValues.height;
|
||||
prevValues.width = change.width;
|
||||
prevValues.height = change.height;
|
||||
}
|
||||
|
||||
// Fix expandParent when resizing from top/left
|
||||
if (parentNode && node.expandParent) {
|
||||
const xLimit = nodeOrigin[0] * (change.width ?? 0);
|
||||
if (change.x && change.x < xLimit) {
|
||||
prevValues.x = xLimit;
|
||||
startValues.x = startValues.x - (change.x - xLimit);
|
||||
}
|
||||
|
||||
const yLimit = nodeOrigin[1] * (change.height ?? 0);
|
||||
if (change.y && change.y < yLimit) {
|
||||
prevValues.y = yLimit;
|
||||
startValues.y = startValues.y - (change.y - yLimit);
|
||||
}
|
||||
}
|
||||
|
||||
const direction = getResizeDirection({
|
||||
width: prevValues.width,
|
||||
prevWidth,
|
||||
height: prevValues.height,
|
||||
prevHeight,
|
||||
affectsX: controlDirection.affectsX,
|
||||
affectsY: controlDirection.affectsY,
|
||||
});
|
||||
|
||||
const nextValues = { ...prevValues, direction };
|
||||
|
||||
const callResize = shouldResize?.(event, nextValues);
|
||||
|
||||
if (callResize === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
onResize?.(event, nextValues);
|
||||
onChange(change, childChanges);
|
||||
})
|
||||
.on('end', (event: ResizeDragEvent) => {
|
||||
onResizeEnd?.(event, { ...prevValues });
|
||||
onEnd?.();
|
||||
});
|
||||
selection.call(dragHandler);
|
||||
}
|
||||
function destroy() {
|
||||
selection.on('.drag', null);
|
||||
}
|
||||
|
||||
return {
|
||||
update,
|
||||
destroy,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './types';
|
||||
export * from './XYResizer';
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { D3DragEvent, SubjectPosition } from 'd3-drag';
|
||||
|
||||
export type XYResizerParams = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type XYResizerParamsWithDirection = XYResizerParams & {
|
||||
direction: number[];
|
||||
};
|
||||
|
||||
export type ControlLinePosition = 'top' | 'bottom' | 'left' | 'right';
|
||||
|
||||
export type ControlPosition = ControlLinePosition | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
||||
|
||||
export enum ResizeControlVariant {
|
||||
Line = 'line',
|
||||
Handle = 'handle',
|
||||
}
|
||||
|
||||
export const XY_RESIZER_HANDLE_POSITIONS: ControlPosition[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
||||
export const XY_RESIZER_LINE_POSITIONS: ControlLinePosition[] = ['top', 'right', 'bottom', 'left'];
|
||||
|
||||
type OnResizeHandler<Params = XYResizerParams, Result = void> = (event: ResizeDragEvent, params: Params) => Result;
|
||||
export type ResizeDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;
|
||||
|
||||
export type ShouldResize = OnResizeHandler<XYResizerParamsWithDirection, boolean>;
|
||||
export type OnResizeStart = OnResizeHandler;
|
||||
export type OnResize = OnResizeHandler<XYResizerParamsWithDirection>;
|
||||
export type OnResizeEnd = OnResizeHandler;
|
||||
@@ -0,0 +1,280 @@
|
||||
import { CoordinateExtent, NodeOrigin } from '../types';
|
||||
import { getPointerPosition } from '../utils';
|
||||
import { ControlPosition } from './types';
|
||||
|
||||
type GetResizeDirectionParams = {
|
||||
width: number;
|
||||
prevWidth: number;
|
||||
height: number;
|
||||
prevHeight: number;
|
||||
affectsX: boolean;
|
||||
affectsY: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all connecting edges for a given set of nodes
|
||||
* @param width - new width of the node
|
||||
* @param prevWidth - previous width of the node
|
||||
* @param height - new height of the node
|
||||
* @param prevHeight - previous height of the node
|
||||
* @param affectsX - whether to invert the resize direction for the x axis
|
||||
* @param affectsY - whether to invert the resize direction for the y axis
|
||||
* @returns array of two numbers representing the direction of the resize for each axis, 0 = no change, 1 = increase, -1 = decrease
|
||||
*/
|
||||
export function getResizeDirection({
|
||||
width,
|
||||
prevWidth,
|
||||
height,
|
||||
prevHeight,
|
||||
affectsX,
|
||||
affectsY,
|
||||
}: GetResizeDirectionParams) {
|
||||
const deltaWidth = width - prevWidth;
|
||||
const deltaHeight = height - prevHeight;
|
||||
|
||||
const direction = [deltaWidth > 0 ? 1 : deltaWidth < 0 ? -1 : 0, deltaHeight > 0 ? 1 : deltaHeight < 0 ? -1 : 0];
|
||||
|
||||
if (deltaWidth && affectsX) {
|
||||
direction[0] = direction[0] * -1;
|
||||
}
|
||||
|
||||
if (deltaHeight && affectsY) {
|
||||
direction[1] = direction[1] * -1;
|
||||
}
|
||||
return direction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the control position that is being dragged to dimensions that are being resized
|
||||
* @param controlPosition - position of the control that is being dragged
|
||||
* @returns isHorizontal, isVertical, affectsX, affectsY,
|
||||
*/
|
||||
export function getControlDirection(controlPosition: ControlPosition) {
|
||||
const isHorizontal = controlPosition.includes('right') || controlPosition.includes('left');
|
||||
const isVertical = controlPosition.includes('bottom') || controlPosition.includes('top');
|
||||
const affectsX = controlPosition.includes('left');
|
||||
const affectsY = controlPosition.includes('top');
|
||||
|
||||
return {
|
||||
isHorizontal,
|
||||
isVertical,
|
||||
affectsX,
|
||||
affectsY,
|
||||
};
|
||||
}
|
||||
|
||||
type PrevValues = {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
type StartValues = PrevValues & {
|
||||
pointerX: number;
|
||||
pointerY: number;
|
||||
aspectRatio: number;
|
||||
};
|
||||
|
||||
function getLowerExtentClamp(lowerExtent: number, lowerBound: number) {
|
||||
return Math.max(0, lowerBound - lowerExtent);
|
||||
}
|
||||
|
||||
function getUpperExtentClamp(upperExtent: number, upperBound: number) {
|
||||
return Math.max(0, upperExtent - upperBound);
|
||||
}
|
||||
|
||||
function getSizeClamp(size: number, minSize: number, maxSize: number) {
|
||||
return Math.max(0, minSize - size, size - maxSize);
|
||||
}
|
||||
|
||||
function xor(a: boolean, b: boolean) {
|
||||
return a ? !b : b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates new width & height and x & y of node after resize based on pointer position
|
||||
* @description - Buckle up, this is a chunky one... If you want to determine the new dimensions of a node after a resize,
|
||||
* you have to account for all possible restrictions: min/max width/height of the node, the maximum extent the node is allowed
|
||||
* to move in (in this case: resize into) determined by the parent node, the minimal extent determined by child nodes
|
||||
* with expandParent or extent: 'parent' set and oh yeah, these things also have to work with keepAspectRatio!
|
||||
* The way this is done is by determining how much each of these restricting actually restricts the resize and then applying the
|
||||
* strongest restriction. Because the resize affects x, y and width, height and width, height of a opposing side with keepAspectRatio,
|
||||
* the resize amount is always kept in distX & distY amount (the distance in mouse movement)
|
||||
* Instead of clamping each value, we first calculate the biggest 'clamp' (for the lack of a better name) and then apply it to all values.
|
||||
* To complicate things nodeOrigin has to be taken into account as well. This is done by offsetting the nodes as if their origin is [0, 0],
|
||||
* then calculating the restrictions as usual
|
||||
* @param startValues - starting values of resize
|
||||
* @param controlDirection - dimensions affected by the resize
|
||||
* @param pointerPosition - the current pointer position corrected for snapping
|
||||
* @param boundaries - minimum and maximum dimensions of the node
|
||||
* @param keepAspectRatio - prevent changes of asprect ratio
|
||||
* @returns x, y, width and height of the node after resize
|
||||
*/
|
||||
export function getDimensionsAfterResize(
|
||||
startValues: StartValues,
|
||||
controlDirection: ReturnType<typeof getControlDirection>,
|
||||
pointerPosition: ReturnType<typeof getPointerPosition>,
|
||||
boundaries: { minWidth: number; maxWidth: number; minHeight: number; maxHeight: number },
|
||||
keepAspectRatio: boolean,
|
||||
nodeOrigin: NodeOrigin,
|
||||
extent?: CoordinateExtent,
|
||||
childExtent?: CoordinateExtent
|
||||
) {
|
||||
let { affectsX, affectsY } = controlDirection;
|
||||
const { isHorizontal, isVertical } = controlDirection;
|
||||
const isDiagonal = isHorizontal && isVertical;
|
||||
|
||||
const { xSnapped, ySnapped } = pointerPosition;
|
||||
const { minWidth, maxWidth, minHeight, maxHeight } = boundaries;
|
||||
|
||||
const { x: startX, y: startY, width: startWidth, height: startHeight, aspectRatio } = startValues;
|
||||
let distX = Math.floor(isHorizontal ? xSnapped - startValues.pointerX : 0);
|
||||
let distY = Math.floor(isVertical ? ySnapped - startValues.pointerY : 0);
|
||||
|
||||
const newWidth = startWidth + (affectsX ? -distX : distX);
|
||||
const newHeight = startHeight + (affectsY ? -distY : distY);
|
||||
|
||||
const originOffsetX = -nodeOrigin[0] * startWidth;
|
||||
const originOffsetY = -nodeOrigin[1] * startHeight;
|
||||
|
||||
// Check if maxWidth, minWWidth, maxHeight, minHeight are restricting the resize
|
||||
let clampX = getSizeClamp(newWidth, minWidth, maxWidth);
|
||||
let clampY = getSizeClamp(newHeight, minHeight, maxHeight);
|
||||
|
||||
// Check if extent is restricting the resize
|
||||
if (extent) {
|
||||
let xExtentClamp = 0;
|
||||
let yExtentClamp = 0;
|
||||
if (affectsX && distX < 0) {
|
||||
xExtentClamp = getLowerExtentClamp(startX + distX + originOffsetX, extent[0][0]);
|
||||
} else if (!affectsX && distX > 0) {
|
||||
xExtentClamp = getUpperExtentClamp(startX + newWidth + originOffsetX, extent[1][0]);
|
||||
}
|
||||
|
||||
if (affectsY && distY < 0) {
|
||||
yExtentClamp = getLowerExtentClamp(startY + distY + originOffsetY, extent[0][1]);
|
||||
} else if (!affectsY && distY > 0) {
|
||||
yExtentClamp = getUpperExtentClamp(startY + newHeight + originOffsetY, extent[1][1]);
|
||||
}
|
||||
|
||||
clampX = Math.max(clampX, xExtentClamp);
|
||||
clampY = Math.max(clampY, yExtentClamp);
|
||||
}
|
||||
|
||||
// Check if the child extent is restricting the resize
|
||||
if (childExtent) {
|
||||
let xExtentClamp = 0;
|
||||
let yExtentClamp = 0;
|
||||
if (affectsX && distX > 0) {
|
||||
xExtentClamp = getUpperExtentClamp(startX + distX, childExtent[0][0]);
|
||||
} else if (!affectsX && distX < 0) {
|
||||
xExtentClamp = getLowerExtentClamp(startX + newWidth, childExtent[1][0]);
|
||||
}
|
||||
|
||||
if (affectsY && distY > 0) {
|
||||
yExtentClamp = getUpperExtentClamp(startY + distY, childExtent[0][1]);
|
||||
} else if (!affectsY && distY < 0) {
|
||||
yExtentClamp = getLowerExtentClamp(startY + newHeight, childExtent[1][1]);
|
||||
}
|
||||
|
||||
clampX = Math.max(clampX, xExtentClamp);
|
||||
clampY = Math.max(clampY, yExtentClamp);
|
||||
}
|
||||
|
||||
// Check if the aspect ratio resizing of the other side is restricting the resize
|
||||
if (keepAspectRatio) {
|
||||
if (isHorizontal) {
|
||||
// Check if the max dimensions might be restricting the resize
|
||||
const aspectHeightClamp = getSizeClamp(newWidth / aspectRatio, minHeight, maxHeight) * aspectRatio;
|
||||
clampX = Math.max(clampX, aspectHeightClamp);
|
||||
|
||||
// Check if the extent is restricting the resize
|
||||
if (extent) {
|
||||
let aspectExtentClamp = 0;
|
||||
if ((!affectsX && !affectsY) || (affectsX && !affectsY && isDiagonal)) {
|
||||
aspectExtentClamp =
|
||||
getUpperExtentClamp(startY + originOffsetY + newWidth / aspectRatio, extent[1][1]) * aspectRatio;
|
||||
} else {
|
||||
aspectExtentClamp =
|
||||
getLowerExtentClamp(startY + originOffsetY + (affectsX ? distX : -distX) / aspectRatio, extent[0][1]) *
|
||||
aspectRatio;
|
||||
}
|
||||
clampX = Math.max(clampX, aspectExtentClamp);
|
||||
}
|
||||
|
||||
// Check if the child extent is restricting the resize
|
||||
if (childExtent) {
|
||||
let aspectExtentClamp = 0;
|
||||
if ((!affectsX && !affectsY) || (affectsX && !affectsY && isDiagonal)) {
|
||||
aspectExtentClamp = getLowerExtentClamp(startY + newWidth / aspectRatio, childExtent[1][1]) * aspectRatio;
|
||||
} else {
|
||||
aspectExtentClamp =
|
||||
getUpperExtentClamp(startY + (affectsX ? distX : -distX) / aspectRatio, childExtent[0][1]) * aspectRatio;
|
||||
}
|
||||
clampX = Math.max(clampX, aspectExtentClamp);
|
||||
}
|
||||
}
|
||||
|
||||
// Do the same thing for vertical resizing
|
||||
if (isVertical) {
|
||||
const aspectWidthClamp = getSizeClamp(newHeight * aspectRatio, minWidth, maxWidth) / aspectRatio;
|
||||
clampY = Math.max(clampY, aspectWidthClamp);
|
||||
|
||||
if (extent) {
|
||||
let aspectExtentClamp = 0;
|
||||
if ((!affectsX && !affectsY) || (affectsY && !affectsX && isDiagonal)) {
|
||||
aspectExtentClamp =
|
||||
getUpperExtentClamp(startX + newHeight * aspectRatio + originOffsetX, extent[1][0]) / aspectRatio;
|
||||
} else {
|
||||
aspectExtentClamp =
|
||||
getLowerExtentClamp(startX + (affectsY ? distY : -distY) * aspectRatio + originOffsetX, extent[0][0]) /
|
||||
aspectRatio;
|
||||
}
|
||||
clampY = Math.max(clampY, aspectExtentClamp);
|
||||
}
|
||||
|
||||
if (childExtent) {
|
||||
let aspectExtentClamp = 0;
|
||||
if ((!affectsX && !affectsY) || (affectsY && !affectsX && isDiagonal)) {
|
||||
aspectExtentClamp = getLowerExtentClamp(startX + newHeight * aspectRatio, childExtent[1][0]) / aspectRatio;
|
||||
} else {
|
||||
aspectExtentClamp =
|
||||
getUpperExtentClamp(startX + (affectsY ? distY : -distY) * aspectRatio, childExtent[0][0]) / aspectRatio;
|
||||
}
|
||||
clampY = Math.max(clampY, aspectExtentClamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
distY = distY + (distY < 0 ? clampY : -clampY);
|
||||
distX = distX + (distX < 0 ? clampX : -clampX);
|
||||
|
||||
if (keepAspectRatio) {
|
||||
if (isDiagonal) {
|
||||
if (newWidth > newHeight * aspectRatio) {
|
||||
distY = (xor(affectsX, affectsY) ? -distX : distX) / aspectRatio;
|
||||
} else {
|
||||
distX = (xor(affectsX, affectsY) ? -distY : distY) * aspectRatio;
|
||||
}
|
||||
} else {
|
||||
if (isHorizontal) {
|
||||
distY = distX / aspectRatio;
|
||||
affectsY = affectsX;
|
||||
} else {
|
||||
distX = distY * aspectRatio;
|
||||
affectsX = affectsY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const x = affectsX ? startX + distX : startX;
|
||||
const y = affectsY ? startY + distY : startY;
|
||||
|
||||
return {
|
||||
width: startWidth + (affectsX ? -distX : distX),
|
||||
height: startHeight + (affectsY ? -distY : distY),
|
||||
x: nodeOrigin[0] * distX * (!affectsX ? 1 : -1) + x,
|
||||
y: nodeOrigin[1] * distY * (!affectsY ? 1 : -1) + y,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user