diff --git a/CHANGELOG.md b/CHANGELOG.md
index 144187220..e3ddcfa34 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- implemented support for `intent` states in code editor
- `Label` component
- added `additionalElements` property to display elements at the end of the label
+- ``
+ - `resizeDirections` to specifiy the axis that can be used to resize the node
+ - `resizeMaxDimensions` to add maximum values for resizing height/width
## [24.0.1] - 2025-02-06
diff --git a/package.json b/package.json
index 7aa2554aa..3abae77c1 100644
--- a/package.json
+++ b/package.json
@@ -88,7 +88,7 @@
"jshint": "^2.13.6",
"lodash": "^4.17.21",
"n3": "^1.23.1",
- "re-resizable": "^6.10.1",
+ "re-resizable": "^6.10.3",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-flow-renderer": "9.7.4",
diff --git a/src/components/Menu/menu.scss b/src/components/Menu/menu.scss
index dff9f4961..431bc386e 100644
--- a/src/components/Menu/menu.scss
+++ b/src/components/Menu/menu.scss
@@ -48,6 +48,7 @@ $menu-background-color: transparent !default;
@import "~@blueprintjs/core/src/components/menu/menu";
.#{$ns}-menu {
+ min-width: auto;
padding: 0;
.#{$ns}-popover2-content > & {
diff --git a/src/extensions/codemirror/CodeMirror.tsx b/src/extensions/codemirror/CodeMirror.tsx
index 3a040b06f..727a78c6d 100644
--- a/src/extensions/codemirror/CodeMirror.tsx
+++ b/src/extensions/codemirror/CodeMirror.tsx
@@ -1,7 +1,6 @@
import React, { useMemo, useRef } from "react";
import { defaultKeymap, indentWithTab } from "@codemirror/commands";
import { foldKeymap } from "@codemirror/language";
-import { lintGutter } from "@codemirror/lint";
import { EditorState, Extension } from "@codemirror/state";
import { DOMEventHandlers, EditorView, KeyBinding, keymap, Rect, ViewUpdate } from "@codemirror/view";
import { minimalSetup } from "codemirror";
@@ -29,6 +28,7 @@ import {
adaptedHighlightSpecialChars,
adaptedLineNumbers,
adaptedPlaceholder,
+ adaptedLintGutter,
} from "./tests/codemirrorTestHelper";
import { ExtensionCreator } from "./types";
@@ -212,7 +212,7 @@ export const CodeEditor = ({
return [];
}
- const values = [lintGutter()];
+ const values = [adaptedLintGutter()];
const linters = ModeLinterMap.get(mode);
if (linters) {
@@ -320,24 +320,26 @@ export const CodeEditor = ({
parent: parent.current,
});
- if (height) {
- view.dom.style.height = typeof height === "string" ? height : `${height}px`;
- }
+ if (view?.dom) {
+ if (height) {
+ view.dom.style.height = typeof height === "string" ? height : `${height}px`;
+ }
- if (disabled) {
- view.dom.className += ` ${eccgui}-disabled`;
- }
+ if (disabled) {
+ view.dom.className += ` ${eccgui}-disabled`;
+ }
- if (intent) {
- view.dom.className += ` ${eccgui}-intent--${intent}`;
- }
+ if (intent) {
+ view.dom.className += ` ${eccgui}-intent--${intent}`;
+ }
- if (autoFocus) {
- view.focus();
- }
+ if (autoFocus) {
+ view.focus();
+ }
- if (setEditorView) {
- setEditorView(view);
+ if (setEditorView) {
+ setEditorView(view);
+ }
}
return () => {
diff --git a/src/extensions/codemirror/tests/codemirrorTestHelper.ts b/src/extensions/codemirror/tests/codemirrorTestHelper.ts
index f7a38162c..8d1c35e26 100644
--- a/src/extensions/codemirror/tests/codemirrorTestHelper.ts
+++ b/src/extensions/codemirror/tests/codemirrorTestHelper.ts
@@ -10,6 +10,7 @@
import { EditorView, placeholder, highlightSpecialChars, lineNumbers, highlightActiveLine } from "@codemirror/view";
import { syntaxHighlighting, foldGutter, codeFolding } from "@codemirror/language";
import { Extension } from "@codemirror/state";
+import { lintGutter } from "@codemirror/lint";
/** placeholder extension, current error '_view.placeholder is not a function' */
export const adaptedPlaceholder = (text?: string) =>
@@ -55,3 +56,6 @@ export const adaptedFoldGutter = (props?: any) =>
export const adaptedCodeFolding = (props?: any) =>
typeof codeFolding === "function" ? codeFolding(props) : emptyExtension;
+
+export const adaptedLintGutter = (props?: any) =>
+ typeof lintGutter === "function" ? lintGutter(props) : emptyExtension;
diff --git a/src/extensions/react-flow/_config.scss b/src/extensions/react-flow/_config.scss
index ad836e86d..d5855aaf4 100644
--- a/src/extensions/react-flow/_config.scss
+++ b/src/extensions/react-flow/_config.scss
@@ -15,3 +15,4 @@ $reactflow-edge-stroke-color-selected: $eccgui-color-accent !default;
$reactflow-transition-time: 0.25s !default;
$reactflow-transition-function: "" !default;
$reactflow-transition-anglestart: -90deg;
+$reactflow-cursor-delimiter-offset: 0.9 * $eccgui-size-block-whitespace;
diff --git a/src/extensions/react-flow/nodes/NodeContent.tsx b/src/extensions/react-flow/nodes/NodeContent.tsx
index b072f51d2..99add1745 100644
--- a/src/extensions/react-flow/nodes/NodeContent.tsx
+++ b/src/extensions/react-flow/nodes/NodeContent.tsx
@@ -23,10 +23,15 @@ type NodeContentHandleNextProps = HandleNextProps;
export type NodeContentHandleProps = NodeContentHandleLegacyProps | NodeContentHandleNextProps;
type NodeDimensions = {
- width: number;
- height: number;
+ width?: number;
+ height?: number;
};
+type ResizeDirections =
+ | { right: true; bottom?: false }
+ | { right?: false; bottom: true }
+ | { right: true; bottom: true };
+
type IntroductionTime = {
/**
* The delay time in ms before the introduction animation is displayed.
@@ -210,6 +215,10 @@ export interface NodeContentProps
* width and height dimensions of the node (Optional)
*/
nodeDimensions?: NodeDimensions;
+ /** if node is resizable, this allows direction of specificity */
+ resizeDirections?: ResizeDirections;
+ /** determines how much width a node can be resized to */
+ resizeMaxDimensions?: Partial;
}
interface MemoHandlerLegacyProps extends HandleProps {
@@ -317,20 +326,24 @@ export function NodeContent({
//handles = defaultHandles(),
adaptHeightForHandleMinCount,
adaptSizeIncrement = 15,
+ // FIXME: getMinimalTooltipData is just being ignored, only used in `NodeDefault`
getMinimalTooltipData = getDefaultMinimalTooltipData,
style = {},
showUnconnectableHandles = false,
animated = false,
introductionTime = 0,
+ // resizing
onNodeResize,
nodeDimensions,
+ resizeDirections = { bottom: true, right: true },
+ resizeMaxDimensions,
// forwarded props
targetPosition = Position.Left,
sourcePosition = Position.Right,
isConnectable = true,
selected,
letPassWheelEvents = false,
- // businessData is just being ignored
+ // FIXME: businessData is just being ignored
businessData,
// other props for DOM element
...otherDomProps
@@ -341,11 +354,16 @@ export function NodeContent({
const { handles = defaultHandles(flowVersionCheck), ...otherProps } = otherDomProps;
- const isResizeable = !!onNodeResize && minimalShape === "none";
- const [width, setWidth] = React.useState(nodeDimensions?.width ?? 0);
- const [height, setHeight] = React.useState(nodeDimensions?.height ?? 0);
+ const hasValidResizeDirection = resizeDirections.bottom || resizeDirections.right;
+ const isResizable = typeof onNodeResize === "function" && hasValidResizeDirection && minimalShape === "none";
+
+ const [width, setWidth] = React.useState(nodeDimensions?.width ?? undefined);
+ const [height, setHeight] = React.useState(nodeDimensions?.height ?? undefined);
+ // Keeps the initial size of the element
+ const originalSize = React.useRef({})
+
let zoom = 1;
- if (isResizeable)
+ if (isResizable)
try {
[, , zoom] =
flowVersionCheck === "legacy"
@@ -371,29 +389,71 @@ export function NodeContent({
handleStack[Position.Left] =
flowVersionCheck === "legacy" ? ([] as NodeContentHandleLegacyProps[]) : ([] as NodeContentHandleNextProps[]);
- // initial dimension before resize
+ const saveOriginalSize = () => {
+ originalSize.current.width = nodeContentRef.current.offsetWidth as number;
+ originalSize.current.height = nodeContentRef.current.offsetHeight as number;
+ }
+
React.useEffect(() => {
- if (!!onNodeResize && minimalShape === "none") {
- if (!nodeDimensions) {
- setWidth(nodeContentRef.current.offsetWidth);
- setHeight(nodeContentRef.current.offsetHeight);
- onNodeResize({
- height: nodeContentRef.current.offsetHeight,
- width: nodeContentRef.current.offsetWidth,
- });
- }
- nodeContentRef.current.className = nodeContentRef.current.className + " is-resizeable";
+ if(nodeContentRef.current && !(originalSize.current.width || originalSize.current.height)) {
+ saveOriginalSize();
}
- }, [nodeContentRef, onNodeResize, minimalShape, nodeDimensions]);
+ }, [!!nodeContentRef.current, !(originalSize.current.width || originalSize.current.height)])
- // update node dimensions when resized
+ // Update width and height when node dimensions parameters has changed
React.useEffect(() => {
- if (nodeDimensions) {
- setWidth(nodeDimensions.width);
- setHeight(nodeDimensions.height);
+ const updateWidth = nodeDimensions?.width ? validateWidth(nodeDimensions?.width) : undefined;
+ const updateHeight = nodeDimensions?.height ? validateHeight(nodeDimensions?.height) : undefined;
+ setWidth(updateWidth);
+ setHeight(updateHeight);
+ if (!nodeDimensions?.width && !nodeDimensions?.height) {
+ // provoke new measuring if no dimensions are set
+ saveOriginalSize();
}
}, [nodeDimensions]);
+ const isResizingActive = React.useCallback((): boolean => {
+ const currentClassNames = nodeContentRef.current.classList;
+ return resizeDirections.right === currentClassNames.contains("is-resizable-horizontal") &&
+ resizeDirections.bottom === currentClassNames.contains("is-resizable-vertical");
+ }, [])
+
+ // force default size when resizing is activated but no dimensions are set
+ React.useEffect(() => {
+ const resizingActive = isResizingActive();
+
+ if (isResizable && !resizingActive) {
+ if (!width || !height) {
+ const newWidth = validateWidth(width ?? originalSize.current?.width as number);
+ const newHeight = validateHeight(height ?? originalSize.current?.height as number);
+ setWidth(newWidth);
+ setHeight(newHeight);
+ }
+ }
+ }, [nodeContentRef.current, onNodeResize, minimalShape, resizeDirections?.bottom, resizeDirections?.right, width, height]); // need to be done everytime a property is changed and the element is re-rendered, otherwise the resizing class is lost
+
+ // conditional enhancements for activated resizing
+ React.useEffect(() => {
+ const currentClassNames = nodeContentRef.current.classList;
+ const resizingActive = isResizingActive();
+
+ if (isResizable && !resizingActive) {
+ if (currentClassNames.contains("is-resizable-horizontal")) {
+ nodeContentRef.current.classList.remove("is-resizable-horizontal");
+ }
+ if (currentClassNames.contains("is-resizable-vertical")) {
+ nodeContentRef.current.classList.remove("is-resizable-vertical");
+ }
+
+ if (resizeDirections.right) {
+ nodeContentRef.current.classList.add("is-resizable-horizontal");
+ }
+ if (resizeDirections.bottom) {
+ nodeContentRef.current.classList.add("is-resizable-vertical");
+ }
+ }
+ }); // need to be done everytime a property is changed and the element is re-rendered, otherwise the resizing class is lost
+
// remove introduction class
React.useEffect(() => {
if (nodeContentRef && introductionTime) {
@@ -460,7 +520,14 @@ export function NodeContent({
);
const resizableStyles =
- !!onNodeResize === true && minimalShape === "none" && width + height > 0 ? { width, height } : {};
+ isResizable && (width ?? 0) + (height ?? 0) > 0
+ ? {
+ width,
+ height,
+ maxWidth: resizeMaxDimensions?.width ?? undefined,
+ maxHeight: resizeMaxDimensions?.height ?? undefined,
+ }
+ : {};
const introductionStyles =
introductionTime && !introductionDone
@@ -470,6 +537,7 @@ export function NodeContent({
}ms`,
} as React.CSSProperties)
: {};
+
const nodeContent = (
<>
({
>
);
- const resizableNode = () => (
- {
- if (nodeContentRef.current) {
- nodeContentRef.current.style.width = width + d.width + "px";
- nodeContentRef.current.style.height = height + d.height + "px";
+ const validateWidth = (resizedWidth: number): number | undefined => {
+ // only allow value if resize direction is allowed
+ if (!resizeDirections.right) {
+ return undefined;
+ }
+ // we need to check because there is probably a min value defined via CSS
+ const min = parseFloat(getComputedStyle(nodeContentRef.current).getPropertyValue("min-width"));
+ // we need to check for a given max value
+ const max = resizeMaxDimensions?.width ?? Infinity;
+ const validatedWidth = Math.max(Math.min(resizedWidth, max), min);
+ return validatedWidth;
+ };
+
+ const validateHeight = (resizedHeight: number): number | undefined => {
+ if (!resizeDirections.bottom) {
+ return undefined;
+ }
+ // we need to check because there is probably a min value defined via CSS
+ const min = parseFloat(getComputedStyle(nodeContentRef.current).getPropertyValue("min-height"));
+ const max = resizeMaxDimensions?.height ?? Infinity;
+ const validatedHeight = Math.max(Math.min(resizedHeight, max), min);
+ return validatedHeight;
+ };
+
+ const resizableNode = () => {
+ const size = { height: height ?? "auto", width: width ?? "auto" };
+ return (
+ {
- setWidth(width + d.width);
- setHeight(height + d.height);
- onNodeResize &&
- onNodeResize({
- height: height + d.height,
- width: width + d.width,
- });
- }}
- >
- {nodeContent}
-
- );
+ handleWrapperClass={`${eccgui}-graphviz__node__resizer--cursorhandles` + " nodrag"}
+ size={size}
+ maxHeight={resizeMaxDimensions?.height ?? undefined}
+ maxWidth={resizeMaxDimensions?.width ?? undefined}
+ enable={resizeDirections.bottom && resizeDirections.right ? { bottomRight: true } : resizeDirections}
+ scale={zoom}
+ onResize={(_0, _1, _2, d) => {
+ if (nodeContentRef.current) {
+ const nextWidth = resizeDirections.right
+ ? (width ?? originalSize.current.width ?? 0) + d.width
+ : undefined;
+ const nextHeight = resizeDirections.bottom
+ ? (height ?? originalSize.current.height ?? 0) + d.height
+ : undefined;
+ if (nextWidth) {
+ nodeContentRef.current.style.width = `${nextWidth}px`;
+ }
+ if (nextHeight) {
+ nodeContentRef.current.style.height = `${nextHeight}px`;
+ }
+ }
+ }}
+ onResizeStop={(_0, _1, _2, d) => {
+ const nextWidth = validateWidth((width ?? originalSize.current.width ?? 0) + d.width);
+ const nextHeight = validateHeight((height ?? originalSize.current.height ?? 0) + d.height);
+ setWidth(nextWidth);
+ setHeight(nextHeight);
+ if (onNodeResize) {
+ onNodeResize({
+ height: nextHeight,
+ width: nextWidth,
+ });
+ }
+ }}
+ >
+ {nodeContent}
+
+ );
+ };
- return isResizeable ? resizableNode() : nodeContent;
+ return isResizable ? resizableNode() : nodeContent;
}
const evaluateHighlightColors = (
@@ -644,7 +758,7 @@ const evaluateHighlightColors = (
let customColor = Color("#ffffff");
try {
customColor = Color(color);
- } catch (ex) {
+ } catch {
// eslint-disable-next-line no-console
console.warn("Received invalid color for highlight: " + color);
}
diff --git a/src/extensions/react-flow/nodes/_nodes.scss b/src/extensions/react-flow/nodes/_nodes.scss
index f7207f958..f87bb701b 100644
--- a/src/extensions/react-flow/nodes/_nodes.scss
+++ b/src/extensions/react-flow/nodes/_nodes.scss
@@ -49,6 +49,38 @@
&:hover {
box-shadow: 0 0 0 6 * $reactflow-node-border-width rgba($reactflow-edge-stroke-color-selected, 0.05);
}
+
+ &.is-resizable-vertical {
+ max-height: unset;
+ }
+}
+
+.#{$eccgui}-graphviz__node--tiny {
+ width: $reactflow-node-basesize * 4;
+ min-height: $reactflow-node-basesize;
+ max-height: $reactflow-node-basesize * 4;
+}
+
+.#{$eccgui}-graphviz__node--small {
+ width: $reactflow-node-basesize * 5;
+ min-height: $reactflow-node-basesize;
+ max-height: $reactflow-node-basesize * 8;
+}
+
+.#{$eccgui}-graphviz__node--medium {
+ width: $reactflow-node-basesize * 8;
+ min-height: $reactflow-node-basesize;
+ max-height: $reactflow-node-basesize * 13;
+}
+
+.#{$eccgui}-graphviz__node--large {
+ width: $reactflow-node-basesize * 13;
+ min-height: $reactflow-node-basesize;
+ max-height: $reactflow-node-basesize * 13;
+}
+
+.#{$eccgui}-graphviz__node--fullwidth {
+ width: 100%;
}
.#{$eccgui}-graphviz__node--minimal-rectangular,
@@ -200,54 +232,58 @@
background-color: $reactflow-node-background-color;
}
-// Node sizes
+// Node Resizer
-.#{$eccgui}-graphviz__node__resizer,
-.#{$eccgui}-graphviz__node.is-resizeable {
+.#{$eccgui}-graphviz__node.is-resizable-horizontal,
+.#{$eccgui}-graphviz__node.is-resizable-vertical {
min-width: 2.5 * $reactflow-node-basesize;
min-height: 2.5 * $reactflow-node-basesize;
}
+.#{$eccgui}-graphviz__node__resizer--cursorhandles {
+ position: absolute;
+ height: 0;
+ width: 0;
+ bottom: 0;
+ right: 0;
+ overflow: visible;
+ display: none;
+
+ & > div {
+ overflow: visible;
+ z-index: 0 !important;
+ height: $reactflow-cursor-delimiter-offset * 3 !important;
+ width: $reactflow-cursor-delimiter-offset * 3 !important;
+ top: unset !important;
+ left: unset !important;
+ bottom: -1 * $reactflow-cursor-delimiter-offset !important;
+ right: -1 * $reactflow-cursor-delimiter-offset !important;
+ border-bottom: $reactflow-node-border-width solid $reactflow-node-border-color;
+ border-right: $reactflow-node-border-width solid $reactflow-node-border-color;
+
+ .#{$eccgui}-graphviz__node__resizer--right:not(.#{$eccgui}-graphviz__node__resizer--bottom) & {
+ bottom: 0 !important;
+ border-bottom: none;
+ }
+ .#{$eccgui}-graphviz__node__resizer--bottom:not(.#{$eccgui}-graphviz__node__resizer--right) & {
+ right: 0 !important;
+ border-right: none;
+ }
+ }
+}
+
.#{$eccgui}-graphviz__node__resizer {
+ min-width: 2.5 * $reactflow-node-basesize;
+ min-height: 2.5 * $reactflow-node-basesize;
+
.selected &,
&:hover {
.#{$eccgui}-graphviz__node__resizer--cursorhandles {
- & > div {
- overflow: auto;
- resize: both;
- }
+ display: block;
}
}
}
-.#{$eccgui}-graphviz__node--tiny:not(.is-resizeable) {
- width: $reactflow-node-basesize * 4;
- min-height: $reactflow-node-basesize;
- max-height: $reactflow-node-basesize * 4;
-}
-
-.#{$eccgui}-graphviz__node--small:not(.is-resizeable) {
- width: $reactflow-node-basesize * 5;
- min-height: $reactflow-node-basesize;
- max-height: $reactflow-node-basesize * 8;
-}
-
-.#{$eccgui}-graphviz__node--medium:not(.is-resizeable) {
- width: $reactflow-node-basesize * 8;
- min-height: $reactflow-node-basesize;
- max-height: $reactflow-node-basesize * 13;
-}
-
-.#{$eccgui}-graphviz__node--large:not(.is-resizeable) {
- width: $reactflow-node-basesize * 13;
- min-height: $reactflow-node-basesize;
- max-height: $reactflow-node-basesize * 13;
-}
-
-.#{$eccgui}-graphviz__node--fullwidth:not(.is-resizeable) {
- width: 100%;
-}
-
// Node border overwrites
.#{$eccgui}-graphviz__node--border-solid {
diff --git a/src/extensions/react-flow/nodes/stories/NodeContent.stories.tsx b/src/extensions/react-flow/nodes/stories/NodeContent.stories.tsx
index 828fbf8b0..68f76a284 100644
--- a/src/extensions/react-flow/nodes/stories/NodeContent.stories.tsx
+++ b/src/extensions/react-flow/nodes/stories/NodeContent.stories.tsx
@@ -15,9 +15,9 @@ import {
OverflowText,
Tag,
TagList,
+ NodeContent,
+ NodeContentExtension,
} from "./../../../../index";
-import { NodeContent } from "./../NodeContent";
-import { NodeContentExtension } from "./../NodeContentExtension";
import {
Default as ContentExtensionExample,
SlideOutOfNode as ContentExtensionExampleSlideOut,
@@ -131,7 +131,6 @@ export default {
const NodeContentExample = (args: any) => {
const [reactflowInstance, setReactflowInstance] = useState(null);
const [elements, setElements] = useState([] as Elements);
- //const [edgeTools, setEdgeTools] = useState(<>>);
useEffect(() => {
setElements([
@@ -206,7 +205,11 @@ Default.args = {
export const Resizeable = Template.bind({});
Resizeable.args = {
...Default.args,
+ resizeMaxDimensions: { width: 1000, height: 500 },
+ nodeDimensions: {},
+ resizeDirections: { bottom: true, right: true },
onNodeResize: (dimensions) => {
+ // eslint-disable-next-line no-console
console.log("onNodeResize", `new dimensions: ${dimensions.width}x${dimensions.height}`);
},
};