diff --git a/apps/docs/src/content/docs/api/Other/ActionStateManager.md b/apps/docs/src/content/docs/api/Other/ActionStateManager.md new file mode 100644 index 000000000..c9f039d6f --- /dev/null +++ b/apps/docs/src/content/docs/api/Other/ActionStateManager.md @@ -0,0 +1,369 @@ +--- +editUrl: false +next: false +prev: false +title: "ActionStateManager" +--- + +**Internal manager** for temporary state during ongoing user actions. +Tracks the state of interactive operations like resizing, linking, rotating, and dragging +until the action completes. + +## Remarks + +**For application code, use [NgDiagramService.actionState](/docs/api/services/ngdiagramservice/#actionstate) signal instead.** +This class is exposed primarily for middleware development where you can access it +via `context.actionStateManager`. + +## Example + +```typescript +const middleware: Middleware = { + name: 'resize-validator', + execute: (context, next, cancel) => { + const resizeState = context.actionStateManager.resize; + if (resizeState) { + console.log('Currently resizing node:', resizeState.nodeId); + } + next(); + } +}; +``` + +## Accessors + +### copyPaste + +#### Get Signature + +> **get** **copyPaste**(): `undefined` \| `CopyPasteActionState` + +Gets the current copy/paste action state. + +##### Returns + +`undefined` \| `CopyPasteActionState` + +The copy/paste state if a copy/paste operation is in progress, undefined otherwise + +#### Set Signature + +> **set** **copyPaste**(`value`): `void` + +Sets the copy/paste action state. + +##### Parameters + +###### value + +The copy/paste state to set, or undefined to clear + +`undefined` | `CopyPasteActionState` + +##### Returns + +`void` + +*** + +### dragging + +#### Get Signature + +> **get** **dragging**(): `undefined` \| `DraggingActionState` + +Gets the current dragging action state. + +##### Returns + +`undefined` \| `DraggingActionState` + +The dragging state if nodes are being dragged, undefined otherwise + +#### Set Signature + +> **set** **dragging**(`value`): `void` + +Sets the dragging action state. + +##### Parameters + +###### value + +The dragging state to set, or undefined to clear + +`undefined` | `DraggingActionState` + +##### Returns + +`void` + +*** + +### highlightGroup + +#### Get Signature + +> **get** **highlightGroup**(): `undefined` \| `HighlightGroupActionState` + +Gets the current highlight group action state. + +##### Returns + +`undefined` \| `HighlightGroupActionState` + +The highlight group state if a group is being highlighted, undefined otherwise + +#### Set Signature + +> **set** **highlightGroup**(`value`): `void` + +Sets the highlight group action state. + +##### Parameters + +###### value + +The highlight group state to set, or undefined to clear + +`undefined` | `HighlightGroupActionState` + +##### Returns + +`void` + +*** + +### linking + +#### Get Signature + +> **get** **linking**(): `undefined` \| `LinkingActionState` + +Gets the current linking action state. + +##### Returns + +`undefined` \| `LinkingActionState` + +The linking state if a link is being created, undefined otherwise + +#### Set Signature + +> **set** **linking**(`value`): `void` + +Sets the linking action state. + +##### Parameters + +###### value + +The linking state to set, or undefined to clear + +`undefined` | `LinkingActionState` + +##### Returns + +`void` + +*** + +### resize + +#### Get Signature + +> **get** **resize**(): `undefined` \| `ResizeActionState` + +Gets the current resize action state. + +##### Returns + +`undefined` \| `ResizeActionState` + +The resize state if a resize is in progress, undefined otherwise + +#### Set Signature + +> **set** **resize**(`value`): `void` + +Sets the resize action state. + +##### Parameters + +###### value + +The resize state to set, or undefined to clear + +`undefined` | `ResizeActionState` + +##### Returns + +`void` + +*** + +### rotation + +#### Get Signature + +> **get** **rotation**(): `undefined` \| `RotationActionState` + +Gets the current rotation action state. + +##### Returns + +`undefined` \| `RotationActionState` + +The rotation state if a rotation is in progress, undefined otherwise + +#### Set Signature + +> **set** **rotation**(`value`): `void` + +Sets the rotation action state. + +##### Parameters + +###### value + +The rotation state to set, or undefined to clear + +`undefined` | `RotationActionState` + +##### Returns + +`void` + +## Methods + +### clearCopyPaste() + +> **clearCopyPaste**(): `void` + +Clears the copy/paste action state. + +#### Returns + +`void` + +*** + +### clearDragging() + +> **clearDragging**(): `void` + +Clears the dragging action state. + +#### Returns + +`void` + +*** + +### clearHighlightGroup() + +> **clearHighlightGroup**(): `void` + +Clears the highlight group action state. + +#### Returns + +`void` + +*** + +### clearLinking() + +> **clearLinking**(): `void` + +Clears the linking action state. + +#### Returns + +`void` + +*** + +### clearResize() + +> **clearResize**(): `void` + +Clears the resize action state. + +#### Returns + +`void` + +*** + +### clearRotation() + +> **clearRotation**(): `void` + +Clears the rotation action state. + +#### Returns + +`void` + +*** + +### getState() + +> **getState**(): `Readonly`\<[`ActionState`](/docs/api/types/actionstate/)\> + +Gets the current action state (readonly). + +#### Returns + +`Readonly`\<[`ActionState`](/docs/api/types/actionstate/)\> + +The complete action state object + +*** + +### isDragging() + +> **isDragging**(): `boolean` + +Checks if a dragging operation is currently in progress. + +#### Returns + +`boolean` + +*** + +### isLinking() + +> **isLinking**(): `boolean` + +Checks if a linking operation is currently in progress. + +#### Returns + +`boolean` + +*** + +### isResizing() + +> **isResizing**(): `boolean` + +Checks if a resize operation is currently in progress. + +#### Returns + +`boolean` + +*** + +### isRotating() + +> **isRotating**(): `boolean` + +Checks if a rotation operation is currently in progress. + +#### Returns + +`boolean` diff --git a/apps/docs/src/content/docs/api/Other/EdgeRoutingManager.md b/apps/docs/src/content/docs/api/Other/EdgeRoutingManager.md new file mode 100644 index 000000000..635880fc0 --- /dev/null +++ b/apps/docs/src/content/docs/api/Other/EdgeRoutingManager.md @@ -0,0 +1,308 @@ +--- +editUrl: false +next: false +prev: false +title: "EdgeRoutingManager" +--- + +**Internal manager** for registration, selection, and execution of edge routing implementations. + +## Remarks + +**For application code, use [NgDiagramService](/docs/api/services/ngdiagramservice/) routing methods instead.** +This class is exposed primarily for middleware development where you can access it +via `context.edgeRoutingManager`. + +The manager comes pre-populated with built-in routings (`orthogonal`, `bezier`, `polyline`). +You can register custom routings at runtime. + +## Example + +```typescript +const middleware: Middleware = { + name: 'routing-optimizer', + execute: (context, next) => { + const routingManager = context.edgeRoutingManager; + const defaultRouting = routingManager.getDefaultRouting(); + console.log('Using routing:', defaultRouting); + next(); + } +}; +``` + +## Methods + +### computePath() + +> **computePath**(`routingName`, `points`): `string` + +Computes an SVG path string for the given points using the specified routing. + +#### Parameters + +##### routingName + +The routing to use. If omitted or undefined, the default routing is used. + +`undefined` | [`EdgeRoutingName`](/docs/api/types/edgeroutingname/) + +##### points + +[`Point`](/docs/api/types/point/)[] + +The points to convert into an SVG path string + +#### Returns + +`string` + +An SVG path string suitable for the `d` attribute of an SVG `` element + +#### Throws + +Will throw if the resolved routing is not registered + +#### Example + +```typescript +const points = [{ x: 0, y: 0 }, { x: 100, y: 100 }, { x: 200, y: 100 }]; +const path = routingManager.computePath('polyline', points); +// Returns: "M 0 0 L 100 100 L 200 100" +``` + +*** + +### computePointOnPath() + +> **computePointOnPath**(`routingName`, `points`, `percentage`): [`Point`](/docs/api/types/point/) + +Computes a point along the path at a given percentage. + +#### Parameters + +##### routingName + +The routing to use. If omitted or undefined, the default routing is used. + +`undefined` | [`EdgeRoutingName`](/docs/api/types/edgeroutingname/) + +##### points + +[`Point`](/docs/api/types/point/)[] + +The path points + +##### percentage + +`number` + +Position along the path in range [0, 1] where 0 = start, 1 = end + +#### Returns + +[`Point`](/docs/api/types/point/) + +The interpolated point on the path + +#### Remarks + +If the selected routing implements `computePointOnPath`, it will be used. +Otherwise, falls back to linear interpolation between the first and last points. + +#### Throws + +Will throw if the resolved routing is not registered + +#### Example + +```typescript +const points = [{ x: 0, y: 0 }, { x: 100, y: 100 }]; +const midpoint = routingManager.computePointOnPath('polyline', points, 0.5); +// Returns: { x: 50, y: 50 } +const quarterPoint = routingManager.computePointOnPath('polyline', points, 0.25); +// Returns: { x: 25, y: 25 } +``` + +*** + +### computePoints() + +> **computePoints**(`routingName`, `context`): [`Point`](/docs/api/types/point/)[] + +Computes the routed points for an edge using the specified routing algorithm. + +#### Parameters + +##### routingName + +The routing to use. If omitted or undefined, the default routing is used. + +`undefined` | [`EdgeRoutingName`](/docs/api/types/edgeroutingname/) + +##### context + +[`EdgeRoutingContext`](/docs/api/types/edgeroutingcontext/) + +The routing context containing source/target nodes, ports, edge data, etc. + +#### Returns + +[`Point`](/docs/api/types/point/)[] + +The computed polyline as an array of points + +#### Throws + +Will throw if the resolved routing is not registered + +#### Example + +```typescript +const points = routingManager.computePoints('orthogonal', { + sourceNode: node1, + targetNode: node2, + sourcePosition: { x: 100, y: 50 }, + targetPosition: { x: 300, y: 200 }, + edge: edge +}); +``` + +*** + +### getDefaultRouting() + +> **getDefaultRouting**(): [`EdgeRoutingName`](/docs/api/types/edgeroutingname/) + +Gets the current default routing name. + +#### Returns + +[`EdgeRoutingName`](/docs/api/types/edgeroutingname/) + +The name of the current default routing + +*** + +### getRegisteredRoutings() + +> **getRegisteredRoutings**(): [`EdgeRoutingName`](/docs/api/types/edgeroutingname/)[] + +Gets all registered routing names. + +#### Returns + +[`EdgeRoutingName`](/docs/api/types/edgeroutingname/)[] + +An array of registered routing names (built-in and custom) + +*** + +### getRouting() + +> **getRouting**(`name`): `undefined` \| [`EdgeRouting`](/docs/api/other/edgerouting/) + +Gets a routing implementation by name. + +#### Parameters + +##### name + +[`EdgeRoutingName`](/docs/api/types/edgeroutingname/) + +The routing name to look up + +#### Returns + +`undefined` \| [`EdgeRouting`](/docs/api/other/edgerouting/) + +The routing implementation or `undefined` if not registered + +*** + +### hasRouting() + +> **hasRouting**(`name`): `boolean` + +Checks whether a routing is registered. + +#### Parameters + +##### name + +[`EdgeRoutingName`](/docs/api/types/edgeroutingname/) + +The routing name to check + +#### Returns + +`boolean` + +`true` if registered; otherwise `false` + +*** + +### registerRouting() + +> **registerRouting**(`routing`): `void` + +Registers (or replaces) a routing implementation. + +#### Parameters + +##### routing + +[`EdgeRouting`](/docs/api/other/edgerouting/) + +The routing instance to register. Its name must be non-empty. + +#### Returns + +`void` + +#### Throws + +Will throw if `routing.name` is falsy. + +*** + +### setDefaultRouting() + +> **setDefaultRouting**(`name`): `void` + +Sets the default routing to use for all edges when no specific routing is specified. + +#### Parameters + +##### name + +[`EdgeRoutingName`](/docs/api/types/edgeroutingname/) + +The routing name to set as default + +#### Returns + +`void` + +#### Throws + +Will throw if the routing is not registered + +*** + +### unregisterRouting() + +> **unregisterRouting**(`name`): `void` + +Unregisters a routing by name. + +#### Parameters + +##### name + +[`EdgeRoutingName`](/docs/api/types/edgeroutingname/) + +The routing name to remove + +#### Returns + +`void` diff --git a/apps/docs/src/content/docs/api/Types/EdgeRoutingConfig.md b/apps/docs/src/content/docs/api/Types/EdgeRoutingConfig.md index 24f1cbfcf..f4eafee1b 100644 --- a/apps/docs/src/content/docs/api/Types/EdgeRoutingConfig.md +++ b/apps/docs/src/content/docs/api/Types/EdgeRoutingConfig.md @@ -9,7 +9,7 @@ Configuration for edge routing behavior. ## Indexable -\[`edgeRoutingName`: `string`\]: `undefined` \| [`EdgeRoutingName`](/docs/api/types/edgeroutingname/) \| `Record`\<`string`, `unknown`\> +\[`edgeRoutingName`: `string`\]: `undefined` \| `Record`\<`string`, `unknown`\> \| [`EdgeRoutingName`](/docs/api/types/edgeroutingname/) Allow custom edge routing configurations. diff --git a/apps/docs/src/content/docs/api/Types/FlowState.md b/apps/docs/src/content/docs/api/Types/FlowState.md new file mode 100644 index 000000000..378e15866 --- /dev/null +++ b/apps/docs/src/content/docs/api/Types/FlowState.md @@ -0,0 +1,33 @@ +--- +editUrl: false +next: false +prev: false +title: "FlowState" +--- + +The complete state of the flow diagram. +Represents the current state of all nodes, edges, and metadata. + +## Properties + +### edges + +> **edges**: [`Edge`](/docs/api/types/edge/)\<`object`\>[] + +All edges currently in the diagram + +*** + +### metadata + +> **metadata**: [`Metadata`](/docs/api/types/metadata/) + +Diagram metadata (selection, viewport, etc.) + +*** + +### nodes + +> **nodes**: [`Node`](/docs/api/types/node/)[] + +All nodes currently in the diagram diff --git a/apps/docs/src/content/docs/api/Types/FlowStateUpdate.md b/apps/docs/src/content/docs/api/Types/FlowStateUpdate.md new file mode 100644 index 000000000..cd3044e65 --- /dev/null +++ b/apps/docs/src/content/docs/api/Types/FlowStateUpdate.md @@ -0,0 +1,85 @@ +--- +editUrl: false +next: false +prev: false +title: "FlowStateUpdate" +--- + +Describes a set of changes to apply to the diagram state. +Middlewares can modify state by passing a FlowStateUpdate to the `next()` function. + +## Example + +```typescript +const middleware: Middleware = { + name: 'auto-arranger', + execute: (context, next) => { + // Apply state changes + next({ + nodesToUpdate: [ + { id: 'node1', position: { x: 100, y: 200 } }, + { id: 'node2', position: { x: 300, y: 200 } } + ], + metadataUpdate: { + viewport: { x: 0, y: 0, zoom: 1 } + } + }); + } +}; +``` + +## Properties + +### edgesToAdd? + +> `optional` **edgesToAdd**: [`Edge`](/docs/api/types/edge/)\<`object`\>[] + +Edges to add to the diagram + +*** + +### edgesToRemove? + +> `optional` **edgesToRemove**: `string`[] + +IDs of edges to remove from the diagram + +*** + +### edgesToUpdate? + +> `optional` **edgesToUpdate**: `Partial`\<[`Edge`](/docs/api/types/edge/)\<`object`\>\> & `object`[] + +Partial edge updates (only changed properties need to be specified) + +*** + +### metadataUpdate? + +> `optional` **metadataUpdate**: `Partial`\<[`Metadata`](/docs/api/types/metadata/)\<`object`\>\> + +Partial metadata update (viewport, selection, etc.) + +*** + +### nodesToAdd? + +> `optional` **nodesToAdd**: [`Node`](/docs/api/types/node/)[] + +Nodes to add to the diagram + +*** + +### nodesToRemove? + +> `optional` **nodesToRemove**: `string`[] + +IDs of nodes to remove from the diagram + +*** + +### nodesToUpdate? + +> `optional` **nodesToUpdate**: `Partial`\<[`Node`](/docs/api/types/node/)\> & `object`[] + +Partial node updates (only changed properties need to be specified) diff --git a/apps/docs/src/content/docs/api/Types/Middleware.md b/apps/docs/src/content/docs/api/Types/Middleware.md index 7908ff9a5..767b0f655 100644 --- a/apps/docs/src/content/docs/api/Types/Middleware.md +++ b/apps/docs/src/content/docs/api/Types/Middleware.md @@ -5,7 +5,56 @@ prev: false title: "Middleware" --- -Interface for middleware that can modify the flow state +Middleware interface for intercepting and modifying diagram state changes. + +Middlewares form a chain where each can: +- Inspect the current state and action type +- Modify the state by passing updates to `next()` +- Block operations by calling `cancel()` +- Perform side effects (logging, validation, etc.) + +## Example + +```typescript +// Read-only middleware that blocks modifications +const readOnlyMiddleware: Middleware<'read-only'> = { + name: 'read-only', + execute: (context, next, cancel) => { + const blockedActions = ['addNodes', 'deleteNodes', 'updateNode']; + if (blockedActions.includes(context.modelActionType)) { + console.warn('Action blocked in read-only mode'); + cancel(); + return; + } + next(); + } +}; + +// Auto-snap middleware that modifies positions +const snapMiddleware: Middleware<'auto-snap'> = { + name: 'auto-snap', + execute: (context, next) => { + const gridSize = 20; + const nodesToSnap = context.helpers.getAffectedNodeIds(['position']); + + const updates = nodesToSnap.map(id => { + const node = context.nodesMap.get(id)!; + return { + id, + position: { + x: Math.round(node.position.x / gridSize) * gridSize, + y: Math.round(node.position.y / gridSize) * gridSize + } + }; + }); + + next({ nodesToUpdate: updates }); + } +}; + +// Register middleware +ngDiagramService.registerMiddleware(snapMiddleware); +``` ## Type Parameters @@ -13,7 +62,7 @@ Interface for middleware that can modify the flow state `TName` *extends* `string` = `string` -Type of the name of the middleware (should be a string literal) +The middleware name type (string literal for type safety) ## Properties @@ -21,27 +70,27 @@ Type of the name of the middleware (should be a string literal) > **execute**: (`context`, `next`, `cancel`) => `void` \| `Promise`\<`void`\> -The function that executes the middleware logic +The middleware execution function. #### Parameters ##### context -`MiddlewareContext` +[`MiddlewareContext`](/docs/api/types/middlewarecontext/) -The context of the middleware +Complete context including state, helpers, and configuration ##### next -(`stateUpdate?`) => `Promise`\<`FlowState`\> +(`stateUpdate?`) => `Promise`\<[`FlowState`](/docs/api/types/flowstate/)\> -Function to call to apply the state update and continue to the next middleware +Call this to continue to the next middleware (optionally with state updates) ##### cancel () => `void` -Function to call to cancel the middleware execution +Call this to abort the entire operation #### Returns @@ -53,4 +102,4 @@ Function to call to cancel the middleware execution > **name**: `TName` -The name of the middleware +Unique identifier for the middleware diff --git a/apps/docs/src/content/docs/api/Types/MiddlewareChain.md b/apps/docs/src/content/docs/api/Types/MiddlewareChain.md index 0f8c82c44..c4f21a4d3 100644 --- a/apps/docs/src/content/docs/api/Types/MiddlewareChain.md +++ b/apps/docs/src/content/docs/api/Types/MiddlewareChain.md @@ -7,8 +7,4 @@ title: "MiddlewareChain" > **MiddlewareChain** = [`Middleware`](/docs/api/types/middleware/)[] -Type for middleware chain - -## Template - -Type of the state being modified +An array of middlewares that will be executed in sequence. diff --git a/apps/docs/src/content/docs/api/Types/MiddlewareContext.md b/apps/docs/src/content/docs/api/Types/MiddlewareContext.md new file mode 100644 index 000000000..db4c84137 --- /dev/null +++ b/apps/docs/src/content/docs/api/Types/MiddlewareContext.md @@ -0,0 +1,160 @@ +--- +editUrl: false +next: false +prev: false +title: "MiddlewareContext" +--- + +The context object passed to middleware execute functions. +Provides access to the current state, helper functions, and configuration. + +## Example + +```typescript +const middleware: Middleware = { + name: 'validation', + execute: (context, next, cancel) => { + // Check if any nodes were added + if (context.helpers.anyNodesAdded()) { + console.log('Nodes added:', context.state.nodes); + } + + // Access configuration + console.log('Cell size:', context.config.background.cellSize); + + // Check what action triggered this + if (context.modelActionType === 'addNodes') { + // Validate new nodes + const isValid = validateNodes(context.state.nodes); + if (!isValid) { + cancel(); // Block the operation + return; + } + } + + next(); // Continue to next middleware + } +}; +``` + +## Properties + +### actionStateManager + +> **actionStateManager**: [`ActionStateManager`](/docs/api/other/actionstatemanager/) + +Manager for action states (resizing, linking, etc.) + +*** + +### config + +> **config**: [`FlowConfig`](/docs/api/types/flowconfig/) + +The current diagram configuration + +*** + +### edgeRoutingManager + +> **edgeRoutingManager**: [`EdgeRoutingManager`](/docs/api/other/edgeroutingmanager/) + +Manager for edge routing algorithms + +*** + +### edgesMap + +> **edgesMap**: `Map`\<`string`, [`Edge`](/docs/api/types/edge/)\<`object`\>\> + +Map for quick edge lookup by ID. +Contains the current state after previous middleware processing. +Use this to access edges by ID instead of iterating through `state.edges`. + +*** + +### environment + +> **environment**: [`EnvironmentInfo`](/docs/api/types/environmentinfo/) + +Environment information (browser, rendering engine, etc.) + +*** + +### helpers + +> **helpers**: [`MiddlewareHelpers`](/docs/api/types/middlewarehelpers/) + +Helper functions to check what changed (tracks all cumulative changes from the initial action and all previous middlewares) + +*** + +### history + +> **history**: [`MiddlewareHistoryUpdate`](/docs/api/types/middlewarehistoryupdate/)[] + +All state updates from previous middlewares in the chain + +*** + +### initialEdgesMap + +> **initialEdgesMap**: `Map`\<`string`, [`Edge`](/docs/api/types/edge/)\<`object`\>\> + +The initial edges map before any modifications (before the initial action and before any middleware modifications). +Use this to compare state before and after all modifications. +Common usage: Access removed edge instances that no longer exist in `edgesMap`. + +*** + +### initialNodesMap + +> **initialNodesMap**: `Map`\<`string`, [`Node`](/docs/api/types/node/)\> + +The initial nodes map before any modifications (before the initial action and before any middleware modifications). +Use this to compare state before and after all modifications. +Common usage: Access removed node instances that no longer exist in `nodesMap`. + +*** + +### initialState + +> **initialState**: [`FlowState`](/docs/api/types/flowstate/) + +The state before any modifications (before the initial action and before any middleware modifications) + +*** + +### initialUpdate + +> **initialUpdate**: [`FlowStateUpdate`](/docs/api/types/flowstateupdate/) + +The initial state update that triggered the middleware chain. +Middlewares can add their own updates to the state, so this may not contain all modifications +that will be applied. Use `helpers` to get actual knowledge about all changes. + +*** + +### modelActionType + +> **modelActionType**: [`ModelActionType`](/docs/api/types/modelactiontype/) + +The action that triggered the middleware execution + +*** + +### nodesMap + +> **nodesMap**: `Map`\<`string`, [`Node`](/docs/api/types/node/)\> + +Map for quick node lookup by ID. +Contains the current state after previous middleware processing. +Use this to access nodes by ID instead of iterating through `state.nodes`. + +*** + +### state + +> **state**: [`FlowState`](/docs/api/types/flowstate/) + +The current state (includes the initial modification and all changes from previous middlewares) diff --git a/apps/docs/src/content/docs/api/Types/MiddlewareHelpers.md b/apps/docs/src/content/docs/api/Types/MiddlewareHelpers.md new file mode 100644 index 000000000..c1e4b8304 --- /dev/null +++ b/apps/docs/src/content/docs/api/Types/MiddlewareHelpers.md @@ -0,0 +1,343 @@ +--- +editUrl: false +next: false +prev: false +title: "MiddlewareHelpers" +--- + +Helper functions for checking what changed during middleware execution. +These helpers track all cumulative changes from the initial state update and all previous middlewares. + +## Properties + +### anyEdgesAdded() + +> **anyEdgesAdded**: () => `boolean` + +Checks if any edges were added. + +#### Returns + +`boolean` + +true if at least one edge was added by the initial state update or any previous middleware + +*** + +### anyEdgesRemoved() + +> **anyEdgesRemoved**: () => `boolean` + +Checks if any edges were removed. + +#### Returns + +`boolean` + +true if at least one edge was removed by the initial state update or any previous middleware + +*** + +### anyNodesAdded() + +> **anyNodesAdded**: () => `boolean` + +Checks if any nodes were added. + +#### Returns + +`boolean` + +true if at least one node was added by the initial state update or any previous middleware + +*** + +### anyNodesRemoved() + +> **anyNodesRemoved**: () => `boolean` + +Checks if any nodes were removed. + +#### Returns + +`boolean` + +true if at least one node was removed by the initial state update or any previous middleware + +*** + +### checkIfAnyEdgePropsChanged() + +> **checkIfAnyEdgePropsChanged**: (`props`) => `boolean` + +Checks if any edge has one or more of the specified properties changed. + +#### Parameters + +##### props + +`string`[] + +Array of property names to check (e.g., ['sourcePosition', 'targetPosition']) + +#### Returns + +`boolean` + +true if any edge has any of these properties modified by the initial state update or any previous middleware + +*** + +### checkIfAnyNodePropsChanged() + +> **checkIfAnyNodePropsChanged**: (`props`) => `boolean` + +Checks if any node has one or more of the specified properties changed. + +#### Parameters + +##### props + +`string`[] + +Array of property names to check (e.g., ['position', 'size']) + +#### Returns + +`boolean` + +true if any node has any of these properties modified by the initial state update or any previous middleware + +*** + +### checkIfEdgeAdded() + +> **checkIfEdgeAdded**: (`id`) => `boolean` + +Checks if a specific edge was added. + +#### Parameters + +##### id + +`string` + +The edge ID to check + +#### Returns + +`boolean` + +true if the edge was added by the initial state update or any previous middleware + +*** + +### checkIfEdgeChanged() + +> **checkIfEdgeChanged**: (`id`) => `boolean` + +Checks if a specific edge has been modified. + +#### Parameters + +##### id + +`string` + +The edge ID to check + +#### Returns + +`boolean` + +true if the edge was modified (any property changed) by the initial state update or any previous middleware + +*** + +### checkIfEdgeRemoved() + +> **checkIfEdgeRemoved**: (`id`) => `boolean` + +Checks if a specific edge was removed. + +#### Parameters + +##### id + +`string` + +The edge ID to check + +#### Returns + +`boolean` + +true if the edge was removed by the initial state update or any previous middleware + +*** + +### checkIfNodeAdded() + +> **checkIfNodeAdded**: (`id`) => `boolean` + +Checks if a specific node was added. + +#### Parameters + +##### id + +`string` + +The node ID to check + +#### Returns + +`boolean` + +true if the node was added by the initial state update or any previous middleware + +*** + +### checkIfNodeChanged() + +> **checkIfNodeChanged**: (`id`) => `boolean` + +Checks if a specific node has been modified. + +#### Parameters + +##### id + +`string` + +The node ID to check + +#### Returns + +`boolean` + +true if the node was modified (any property changed) by the initial state update or any previous middleware + +*** + +### checkIfNodeRemoved() + +> **checkIfNodeRemoved**: (`id`) => `boolean` + +Checks if a specific node was removed. + +#### Parameters + +##### id + +`string` + +The node ID to check + +#### Returns + +`boolean` + +true if the node was removed by the initial state update or any previous middleware + +*** + +### getAddedEdges() + +> **getAddedEdges**: () => [`Edge`](/docs/api/types/edge/)\<`object`\>[] + +Gets all edges that were added. + +#### Returns + +[`Edge`](/docs/api/types/edge/)\<`object`\>[] + +Array of edge instances that were added by the initial state update or any previous middleware + +*** + +### getAddedNodes() + +> **getAddedNodes**: () => [`Node`](/docs/api/types/node/)[] + +Gets all nodes that were added. + +#### Returns + +[`Node`](/docs/api/types/node/)[] + +Array of node instances that were added by the initial state update or any previous middleware + +*** + +### getAffectedEdgeIds() + +> **getAffectedEdgeIds**: (`props`) => `string`[] + +Gets all edge IDs that have one or more of the specified properties changed. + +#### Parameters + +##### props + +`string`[] + +Array of property names to check (e.g., ['sourcePosition', 'targetPosition']) + +#### Returns + +`string`[] + +Array of edge IDs that have any of these properties modified by the initial state update or any previous middleware + +*** + +### getAffectedNodeIds() + +> **getAffectedNodeIds**: (`props`) => `string`[] + +Gets all node IDs that have one or more of the specified properties changed. + +#### Parameters + +##### props + +`string`[] + +Array of property names to check (e.g., ['position', 'size']) + +#### Returns + +`string`[] + +Array of node IDs that have any of these properties modified by the initial state update or any previous middleware + +*** + +### getRemovedEdges() + +> **getRemovedEdges**: () => [`Edge`](/docs/api/types/edge/)\<`object`\>[] + +Gets all edges that were removed. +Uses `initialEdgesMap` to access the removed instances. + +#### Returns + +[`Edge`](/docs/api/types/edge/)\<`object`\>[] + +Array of edge instances that were removed by the initial state update or any previous middleware + +*** + +### getRemovedNodes() + +> **getRemovedNodes**: () => [`Node`](/docs/api/types/node/)[] + +Gets all nodes that were removed. +Uses `initialNodesMap` to access the removed instances. + +#### Returns + +[`Node`](/docs/api/types/node/)[] + +Array of node instances that were removed by the initial state update or any previous middleware diff --git a/apps/docs/src/content/docs/api/Types/MiddlewareHistoryUpdate.md b/apps/docs/src/content/docs/api/Types/MiddlewareHistoryUpdate.md new file mode 100644 index 000000000..2e8730095 --- /dev/null +++ b/apps/docs/src/content/docs/api/Types/MiddlewareHistoryUpdate.md @@ -0,0 +1,40 @@ +--- +editUrl: false +next: false +prev: false +title: "MiddlewareHistoryUpdate" +--- + +Records a state update made by a specific middleware. +Used to track the history of state transformations through the middleware chain. + +## Example + +```typescript +const middleware: Middleware = { + name: 'audit-logger', + execute: (context, next) => { + // Check what previous middlewares did + context.history.forEach(update => { + console.log(`${update.name} modified:`, update.stateUpdate); + }); + next(); + } +}; +``` + +## Properties + +### name + +> **name**: `string` + +The name of the middleware that made the update + +*** + +### stateUpdate + +> **stateUpdate**: [`FlowStateUpdate`](/docs/api/types/flowstateupdate/) + +The state update that was applied diff --git a/apps/docs/src/content/docs/api/Types/ModelActionType.md b/apps/docs/src/content/docs/api/Types/ModelActionType.md new file mode 100644 index 000000000..f9da769f4 --- /dev/null +++ b/apps/docs/src/content/docs/api/Types/ModelActionType.md @@ -0,0 +1,23 @@ +--- +editUrl: false +next: false +prev: false +title: "ModelActionType" +--- + +> **ModelActionType** = `"init"` \| `"changeSelection"` \| `"moveNodesBy"` \| `"deleteSelection"` \| `"addNodes"` \| `"updateNode"` \| `"updateNodes"` \| `"deleteNodes"` \| `"clearModel"` \| `"paletteDropNode"` \| `"addEdges"` \| `"updateEdge"` \| `"deleteEdges"` \| `"deleteElements"` \| `"paste"` \| `"moveViewport"` \| `"resizeNode"` \| `"startLinking"` \| `"moveTemporaryEdge"` \| `"finishLinking"` \| `"zoom"` \| `"changeZOrder"` \| `"rotateNodeTo"` \| `"highlightGroup"` \| `"highlightGroupClear"` \| `"moveNodes"` \| `"moveNodesStop"` + +Model action types that can trigger middleware execution. +These represent all possible operations that modify the diagram state. + +## Example + +```typescript +const middleware: Middleware = { + name: 'logger', + execute: (context, next) => { + console.log('Action type:', context.modelActionType); + next(); + } +}; +``` diff --git a/apps/docs/src/content/docs/api/Types/TransactionResult.md b/apps/docs/src/content/docs/api/Types/TransactionResult.md index d630087c0..2e43d2d9e 100644 --- a/apps/docs/src/content/docs/api/Types/TransactionResult.md +++ b/apps/docs/src/content/docs/api/Types/TransactionResult.md @@ -19,6 +19,6 @@ Number of commands emitted during the transaction ### results -> **results**: `FlowStateUpdate` +> **results**: [`FlowStateUpdate`](/docs/api/types/flowstateupdate/) Results of the transaction as a state update diff --git a/apps/docs/src/content/docs/api/_readme.md b/apps/docs/src/content/docs/api/_readme.md index 06d4bc44b..79b81266f 100644 --- a/apps/docs/src/content/docs/api/_readme.md +++ b/apps/docs/src/content/docs/api/_readme.md @@ -24,7 +24,9 @@ title: "ng-diagram" ## Other +- [ActionStateManager](/docs/api/other/actionstatemanager/) - [EdgeRouting](/docs/api/other/edgerouting/) +- [EdgeRoutingManager](/docs/api/other/edgeroutingmanager/) - [KeyboardShortcutBinding](/docs/api/other/keyboardshortcutbinding/) - [KeyboardShortcutDefinition](/docs/api/other/keyboardshortcutdefinition/) - [ModifierOnlyShortcutBinding](/docs/api/other/modifieronlyshortcutbinding/) @@ -67,12 +69,17 @@ title: "ng-diagram" - [EdgeRoutingContext](/docs/api/types/edgeroutingcontext/) - [EnvironmentInfo](/docs/api/types/environmentinfo/) - [FlowConfig](/docs/api/types/flowconfig/) +- [FlowState](/docs/api/types/flowstate/) +- [FlowStateUpdate](/docs/api/types/flowstateupdate/) - [GroupingConfig](/docs/api/types/groupingconfig/) - [GroupMembershipChangedEvent](/docs/api/types/groupmembershipchangedevent/) - [GroupNode](/docs/api/types/groupnode/) - [LinkingConfig](/docs/api/types/linkingconfig/) - [Metadata](/docs/api/types/metadata/) - [Middleware](/docs/api/types/middleware/) +- [MiddlewareContext](/docs/api/types/middlewarecontext/) +- [MiddlewareHelpers](/docs/api/types/middlewarehelpers/) +- [MiddlewareHistoryUpdate](/docs/api/types/middlewarehistoryupdate/) - [Model](/docs/api/types/model/) - [ModelAdapter](/docs/api/types/modeladapter/) - [NgDiagramEdgeTemplate](/docs/api/types/ngdiagramedgetemplate/) @@ -101,6 +108,7 @@ title: "ng-diagram" - [ZoomConfig](/docs/api/types/zoomconfig/) - [EdgeRoutingName](/docs/api/types/edgeroutingname/) - [MiddlewareChain](/docs/api/types/middlewarechain/) +- [ModelActionType](/docs/api/types/modelactiontype/) - [NgDiagramConfig](/docs/api/types/ngdiagramconfig/) - [NgDiagramPaletteItem](/docs/api/types/ngdiagrampaletteitem/) - [Node](/docs/api/types/node/) diff --git a/packages/ng-diagram/projects/ng-diagram/src/core/src/action-state-manager/action-state-manager.ts b/packages/ng-diagram/projects/ng-diagram/src/core/src/action-state-manager/action-state-manager.ts index bebea6a9a..dc9922cff 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/core/src/action-state-manager/action-state-manager.ts +++ b/packages/ng-diagram/projects/ng-diagram/src/core/src/action-state-manager/action-state-manager.ts @@ -10,8 +10,30 @@ import type { } from '../types/action-state.interface'; /** - * Manages temporary state during ongoing actions - * (e.g., resizing or linking) until the action completes. + * **Internal manager** for temporary state during ongoing user actions. + * Tracks the state of interactive operations like resizing, linking, rotating, and dragging + * until the action completes. + * + * @remarks + * **For application code, use {@link NgDiagramService.actionState} signal instead.** + * This class is exposed primarily for middleware development where you can access it + * via `context.actionStateManager`. + * + * @example + * ```typescript + * const middleware: Middleware = { + * name: 'resize-validator', + * execute: (context, next, cancel) => { + * const resizeState = context.actionStateManager.resize; + * if (resizeState) { + * console.log('Currently resizing node:', resizeState.nodeId); + * } + * next(); + * } + * }; + * ``` + * + * @category Other */ export class ActionStateManager { private state: ActionState; @@ -29,98 +51,197 @@ export class ActionStateManager { ); } + /** + * Gets the current action state (readonly). + * + * @returns The complete action state object + */ getState(): Readonly { return this.state; } + /** + * Emits an 'actionStateChanged' event with the current state. + * @internal + */ private emitStateChanged(): void { this.eventManager.emit('actionStateChanged', { actionState: { ...this.state } }); } + /** + * Gets the current resize action state. + * + * @returns The resize state if a resize is in progress, undefined otherwise + */ get resize(): ResizeActionState | undefined { return this.state.resize; } + /** + * Sets the resize action state. + * + * @param value - The resize state to set, or undefined to clear + */ set resize(value: ResizeActionState | undefined) { this.state.resize = value; } + /** + * Gets the current linking action state. + * + * @returns The linking state if a link is being created, undefined otherwise + */ get linking(): LinkingActionState | undefined { return this.state.linking; } + /** + * Sets the linking action state. + * + * @param value - The linking state to set, or undefined to clear + */ set linking(value: LinkingActionState | undefined) { this.state.linking = value; } + /** + * Gets the current copy/paste action state. + * + * @returns The copy/paste state if a copy/paste operation is in progress, undefined otherwise + */ get copyPaste(): CopyPasteActionState | undefined { return this.state.copyPaste; } + /** + * Sets the copy/paste action state. + * + * @param value - The copy/paste state to set, or undefined to clear + */ set copyPaste(value: CopyPasteActionState | undefined) { this.state.copyPaste = value; } + /** + * Gets the current highlight group action state. + * + * @returns The highlight group state if a group is being highlighted, undefined otherwise + */ get highlightGroup(): HighlightGroupActionState | undefined { return this.state.highlightGroup; } + /** + * Sets the highlight group action state. + * + * @param value - The highlight group state to set, or undefined to clear + */ set highlightGroup(value: HighlightGroupActionState | undefined) { this.state.highlightGroup = value; } + /** + * Gets the current rotation action state. + * + * @returns The rotation state if a rotation is in progress, undefined otherwise + */ get rotation(): RotationActionState | undefined { return this.state.rotation; } + /** + * Sets the rotation action state. + * + * @param value - The rotation state to set, or undefined to clear + */ set rotation(value: RotationActionState | undefined) { this.state.rotation = value; } + /** + * Gets the current dragging action state. + * + * @returns The dragging state if nodes are being dragged, undefined otherwise + */ get dragging(): DraggingActionState | undefined { return this.state.dragging; } + /** + * Sets the dragging action state. + * + * @param value - The dragging state to set, or undefined to clear + */ set dragging(value: DraggingActionState | undefined) { this.state.dragging = value; } + /** + * Clears the resize action state. + */ clearResize() { this.state.resize = undefined; } + /** + * Clears the linking action state. + */ clearLinking() { this.state.linking = undefined; } + /** + * Clears the copy/paste action state. + */ clearCopyPaste() { this.state.copyPaste = undefined; } + /** + * Clears the highlight group action state. + */ clearHighlightGroup() { this.state.highlightGroup = undefined; } + /** + * Clears the rotation action state. + */ clearRotation() { this.state.rotation = undefined; } + /** + * Clears the dragging action state. + */ clearDragging() { this.state.dragging = undefined; } + /** + * Checks if a resize operation is currently in progress. + */ isResizing(): boolean { return !!this.state.resize; } + /** + * Checks if a linking operation is currently in progress. + */ isLinking(): boolean { return !!this.state.linking; } + /** + * Checks if a rotation operation is currently in progress. + */ isRotating(): boolean { return !!this.state.rotation; } + /** + * Checks if a dragging operation is currently in progress. + */ isDragging(): boolean { return !!this.state.dragging; } diff --git a/packages/ng-diagram/projects/ng-diagram/src/core/src/edge-routing-manager/edge-routing-manager.ts b/packages/ng-diagram/projects/ng-diagram/src/core/src/edge-routing-manager/edge-routing-manager.ts index 3c80f5d13..042c285e1 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/core/src/edge-routing-manager/edge-routing-manager.ts +++ b/packages/ng-diagram/projects/ng-diagram/src/core/src/edge-routing-manager/edge-routing-manager.ts @@ -6,15 +6,42 @@ import { EdgeRouting, EdgeRoutingContext, EdgeRoutingName } from './types'; /** * List of built-in edge routing names in registration order. + * + * @remarks + * These routings are automatically registered when an EdgeRoutingManager is created: + * - `orthogonal`: Routes edges with right-angle turns + * - `bezier`: Routes edges with smooth Bezier curves + * - `polyline`: Routes edges as straight line segments + * + * @category Other */ export const BUILT_IN_EDGE_ROUTINGS = ['orthogonal', 'bezier', 'polyline'] as const; /** - * Manages registration, selection, and execution of edge routing implementations. + * **Internal manager** for registration, selection, and execution of edge routing implementations. * * @remarks + * **For application code, use {@link NgDiagramService} routing methods instead.** + * This class is exposed primarily for middleware development where you can access it + * via `context.edgeRoutingManager`. + * * The manager comes pre-populated with built-in routings (`orthogonal`, `bezier`, `polyline`). - * You can register custom routings at runtime via {@link EdgeRoutingManager.registerRouting}. + * You can register custom routings at runtime. + * + * @example + * ```typescript + * const middleware: Middleware = { + * name: 'routing-optimizer', + * execute: (context, next) => { + * const routingManager = context.edgeRoutingManager; + * const defaultRouting = routingManager.getDefaultRouting(); + * console.log('Using routing:', defaultRouting); + * next(); + * } + * }; + * ``` + * + * @category Other */ export class EdgeRoutingManager { private routings = new Map(); @@ -22,11 +49,12 @@ export class EdgeRoutingManager { private getRoutingConfig: () => EdgeRoutingConfig; /** - * Creates a new {@link EdgeRoutingManager}. + * Creates a new EdgeRoutingManager and registers built-in routings. + * + * @param defaultEdgeRouting - The routing to use when none is specified (defaults to `'orthogonal'`) + * @param getRoutingConfiguration - Function returning the current routing configuration * - * @param defaultEdgeRouting - The routing to use when none is specified. Defaults to `'orthogonal'`. - * @param getRoutingConfiguration - A function returning the current routing configuration object. - * Defaults to a function that returns an empty object. + * @internal */ constructor(defaultEdgeRouting: EdgeRoutingName, getRoutingConfiguration: () => EdgeRoutingConfig) { this.defaultRouting = defaultEdgeRouting || 'orthogonal'; @@ -40,7 +68,7 @@ export class EdgeRoutingManager { /** * Registers (or replaces) a routing implementation. * - * @param routing - The routing instance to register. Its {@link EdgeRouting.name | name} must be non-empty. + * @param routing - The routing instance to register. Its name must be non-empty. * @throws Will throw if `routing.name` is falsy. */ registerRouting(routing: EdgeRouting): void { @@ -53,17 +81,17 @@ export class EdgeRoutingManager { /** * Unregisters a routing by name. * - * @param name - The routing name to remove. + * @param name - The routing name to remove */ unregisterRouting(name: EdgeRoutingName): void { this.routings.delete(name); } /** - * Gets a routing by name. + * Gets a routing implementation by name. * - * @param name - The routing name to look up. - * @returns The routing implementation or `undefined` if not registered. + * @param name - The routing name to look up + * @returns The routing implementation or `undefined` if not registered */ getRouting(name: EdgeRoutingName): EdgeRouting | undefined { return this.routings.get(name); @@ -72,7 +100,7 @@ export class EdgeRoutingManager { /** * Gets all registered routing names. * - * @returns An array of registered routing names. + * @returns An array of registered routing names (built-in and custom) */ getRegisteredRoutings(): EdgeRoutingName[] { return Array.from(this.routings.keys()); @@ -81,20 +109,31 @@ export class EdgeRoutingManager { /** * Checks whether a routing is registered. * - * @param name - The routing name to check. - * @returns `true` if registered; otherwise `false`. + * @param name - The routing name to check + * @returns `true` if registered; otherwise `false` */ hasRouting(name: EdgeRoutingName): boolean { return this.routings.has(name); } /** - * Computes the routed points for an edge using the specified routing. + * Computes the routed points for an edge using the specified routing algorithm. * - * @param [routingName] - The routing to use. If omitted, the default routing is used. - * @param context - The routing context (source/target nodes, ports, edge etc.). - * @returns The computed polyline as an array of {@link Point}. - * @throws Will throw if the resolved routing is not registered. + * @param routingName - The routing to use. If omitted or undefined, the default routing is used. + * @param context - The routing context containing source/target nodes, ports, edge data, etc. + * @returns The computed polyline as an array of points + * @throws Will throw if the resolved routing is not registered + * + * @example + * ```typescript + * const points = routingManager.computePoints('orthogonal', { + * sourceNode: node1, + * targetNode: node2, + * sourcePosition: { x: 100, y: 50 }, + * targetPosition: { x: 300, y: 200 }, + * edge: edge + * }); + * ``` */ computePoints(routingName: EdgeRoutingName | undefined, context: EdgeRoutingContext): Point[] { const name = routingName || this.defaultRouting; @@ -110,10 +149,17 @@ export class EdgeRoutingManager { /** * Computes an SVG path string for the given points using the specified routing. * - * @param [routingName] - The routing to use. If omitted, the default routing is used. - * @param points - The points to convert into an SVG `d` path string. - * @returns An SVG path string suitable for the `d` attribute of an `` element. - * @throws Will throw if the resolved routing is not registered. + * @param routingName - The routing to use. If omitted or undefined, the default routing is used. + * @param points - The points to convert into an SVG path string + * @returns An SVG path string suitable for the `d` attribute of an SVG `` element + * @throws Will throw if the resolved routing is not registered + * + * @example + * ```typescript + * const points = [{ x: 0, y: 0 }, { x: 100, y: 100 }, { x: 200, y: 100 }]; + * const path = routingManager.computePath('polyline', points); + * // Returns: "M 0 0 L 100 100 L 200 100" + * ``` */ computePath(routingName: EdgeRoutingName | undefined, points: Point[]): string { const name = routingName || this.defaultRouting; @@ -130,18 +176,22 @@ export class EdgeRoutingManager { * Computes a point along the path at a given percentage. * * @remarks - * If the selected routing implements {@link EdgeRouting.computePointOnPath}, it will be used. - * Otherwise, the method falls back to linear interpolation between the first and last points. + * If the selected routing implements `computePointOnPath`, it will be used. + * Otherwise, falls back to linear interpolation between the first and last points. * - * @param [routingName] - The routing to use. If omitted, the default routing is used. - * @param points - The path points. - * @param percentage - Position along the path in `[0, 1]` (0 = start, 1 = end). - * @returns The interpolated {@link Point}. - * @throws Will throw if the resolved routing is not registered. + * @param routingName - The routing to use. If omitted or undefined, the default routing is used. + * @param points - The path points + * @param percentage - Position along the path in range [0, 1] where 0 = start, 1 = end + * @returns The interpolated point on the path + * @throws Will throw if the resolved routing is not registered * * @example - * ```ts - * const p50 = manager.computePointOnPath('polyline', points, 0.5); // midpoint + * ```typescript + * const points = [{ x: 0, y: 0 }, { x: 100, y: 100 }]; + * const midpoint = routingManager.computePointOnPath('polyline', points, 0.5); + * // Returns: { x: 50, y: 50 } + * const quarterPoint = routingManager.computePointOnPath('polyline', points, 0.25); + * // Returns: { x: 25, y: 25 } * ``` */ computePointOnPath(routingName: EdgeRoutingName | undefined, points: Point[], percentage: number): Point { @@ -169,10 +219,10 @@ export class EdgeRoutingManager { } /** - * Sets the default routing. + * Sets the default routing to use for all edges when no specific routing is specified. * - * @param name - The routing name to set as default. - * @throws Will throw if the routing is not registered. + * @param name - The routing name to set as default + * @throws Will throw if the routing is not registered */ setDefaultRouting(name: EdgeRoutingName): void { if (!this.routings.has(name)) { @@ -182,9 +232,9 @@ export class EdgeRoutingManager { } /** - * Gets the default routing name. + * Gets the current default routing name. * - * @returns The current default routing name. + * @returns The name of the current default routing */ getDefaultRouting(): EdgeRoutingName { return this.defaultRouting; diff --git a/packages/ng-diagram/projects/ng-diagram/src/core/src/index.ts b/packages/ng-diagram/projects/ng-diagram/src/core/src/index.ts index ef4163740..f39bf430c 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/core/src/index.ts +++ b/packages/ng-diagram/projects/ng-diagram/src/core/src/index.ts @@ -1,5 +1,6 @@ import './set.polyfill'; +export * from './action-state-manager/action-state-manager'; export * from './edge-routing-manager'; export * from './event-manager'; export * from './flow-core'; diff --git a/packages/ng-diagram/projects/ng-diagram/src/core/src/types/middleware.interface.ts b/packages/ng-diagram/projects/ng-diagram/src/core/src/types/middleware.interface.ts index 150dcb7b7..8b127d215 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/core/src/types/middleware.interface.ts +++ b/packages/ng-diagram/projects/ng-diagram/src/core/src/types/middleware.interface.ts @@ -1,6 +1,5 @@ import type { ActionStateManager } from '../action-state-manager/action-state-manager'; import type { EdgeRoutingManager } from '../edge-routing-manager'; -import type { MiddlewareExecutor } from '../middleware-manager/middleware-executor'; import type { Edge } from './edge.interface'; import { EnvironmentInfo } from './environment.interface'; import { FlowConfig } from './flow-config.interface'; @@ -8,7 +7,21 @@ import type { Metadata } from './metadata.interface'; import type { Node } from './node.interface'; /** - * Type for model-specific actions types in the flow diagram + * Model action types that can trigger middleware execution. + * These represent all possible operations that modify the diagram state. + * + * @example + * ```typescript + * const middleware: Middleware = { + * name: 'logger', + * execute: (context, next) => { + * console.log('Action type:', context.modelActionType); + * next(); + * } + * }; + * ``` + * + * @category Types */ export type ModelActionType = | 'init' @@ -40,104 +53,383 @@ export type ModelActionType = | 'moveNodesStop'; /** - * Type for the state of the flow diagram + * The complete state of the flow diagram. + * Represents the current state of all nodes, edges, and metadata. + * + * @category Types */ export interface FlowState { + /** All nodes currently in the diagram */ nodes: Node[]; + /** All edges currently in the diagram */ edges: Edge[]; + /** Diagram metadata (selection, viewport, etc.) */ metadata: Metadata; } /** - * Type for the history update to be applied to the flow diagram + * Records a state update made by a specific middleware. + * Used to track the history of state transformations through the middleware chain. + * + * @example + * ```typescript + * const middleware: Middleware = { + * name: 'audit-logger', + * execute: (context, next) => { + * // Check what previous middlewares did + * context.history.forEach(update => { + * console.log(`${update.name} modified:`, update.stateUpdate); + * }); + * next(); + * } + * }; + * ``` + * + * @category Types */ export interface MiddlewareHistoryUpdate { + /** The name of the middleware that made the update */ name: string; + /** The state update that was applied */ stateUpdate: FlowStateUpdate; } /** - * Type for the state update to be applied to the flow diagram + * Describes a set of changes to apply to the diagram state. + * Middlewares can modify state by passing a FlowStateUpdate to the `next()` function. + * + * @example + * ```typescript + * const middleware: Middleware = { + * name: 'auto-arranger', + * execute: (context, next) => { + * // Apply state changes + * next({ + * nodesToUpdate: [ + * { id: 'node1', position: { x: 100, y: 200 } }, + * { id: 'node2', position: { x: 300, y: 200 } } + * ], + * metadataUpdate: { + * viewport: { x: 0, y: 0, zoom: 1 } + * } + * }); + * } + * }; + * ``` + * + * @category Types */ export interface FlowStateUpdate { + /** Nodes to add to the diagram */ nodesToAdd?: Node[]; + /** Partial node updates (only changed properties need to be specified) */ nodesToUpdate?: (Partial & { id: Node['id'] })[]; + /** IDs of nodes to remove from the diagram */ nodesToRemove?: string[]; + /** Edges to add to the diagram */ edgesToAdd?: Edge[]; + /** Partial edge updates (only changed properties need to be specified) */ edgesToUpdate?: (Partial & { id: Edge['id'] })[]; + /** IDs of edges to remove from the diagram */ edgesToRemove?: string[]; + /** Partial metadata update (viewport, selection, etc.) */ metadataUpdate?: Partial; } +/** + * Array of middlewares (readonly for type safety). + * + * @category Types + */ export type MiddlewareArray = readonly Middleware[]; /** - * Type for the context of the middleware + * Helper functions for checking what changed during middleware execution. + * These helpers track all cumulative changes from the initial state update and all previous middlewares. + * + * @category Types + */ +export interface MiddlewareHelpers { + /** + * Checks if a specific node has been modified. + * @param id - The node ID to check + * @returns true if the node was modified (any property changed) by the initial state update or any previous middleware + */ + checkIfNodeChanged: (id: string) => boolean; + + /** + * Checks if a specific edge has been modified. + * @param id - The edge ID to check + * @returns true if the edge was modified (any property changed) by the initial state update or any previous middleware + */ + checkIfEdgeChanged: (id: string) => boolean; + + /** + * Checks if a specific node was added. + * @param id - The node ID to check + * @returns true if the node was added by the initial state update or any previous middleware + */ + checkIfNodeAdded: (id: string) => boolean; + + /** + * Checks if a specific node was removed. + * @param id - The node ID to check + * @returns true if the node was removed by the initial state update or any previous middleware + */ + checkIfNodeRemoved: (id: string) => boolean; + + /** + * Checks if a specific edge was added. + * @param id - The edge ID to check + * @returns true if the edge was added by the initial state update or any previous middleware + */ + checkIfEdgeAdded: (id: string) => boolean; + + /** + * Checks if a specific edge was removed. + * @param id - The edge ID to check + * @returns true if the edge was removed by the initial state update or any previous middleware + */ + checkIfEdgeRemoved: (id: string) => boolean; + + /** + * Checks if any node has one or more of the specified properties changed. + * @param props - Array of property names to check (e.g., ['position', 'size']) + * @returns true if any node has any of these properties modified by the initial state update or any previous middleware + */ + checkIfAnyNodePropsChanged: (props: string[]) => boolean; + + /** + * Checks if any edge has one or more of the specified properties changed. + * @param props - Array of property names to check (e.g., ['sourcePosition', 'targetPosition']) + * @returns true if any edge has any of these properties modified by the initial state update or any previous middleware + */ + checkIfAnyEdgePropsChanged: (props: string[]) => boolean; + + /** + * Checks if any nodes were added. + * @returns true if at least one node was added by the initial state update or any previous middleware + */ + anyNodesAdded: () => boolean; + + /** + * Checks if any edges were added. + * @returns true if at least one edge was added by the initial state update or any previous middleware + */ + anyEdgesAdded: () => boolean; + + /** + * Checks if any nodes were removed. + * @returns true if at least one node was removed by the initial state update or any previous middleware + */ + anyNodesRemoved: () => boolean; + + /** + * Checks if any edges were removed. + * @returns true if at least one edge was removed by the initial state update or any previous middleware + */ + anyEdgesRemoved: () => boolean; + + /** + * Gets all node IDs that have one or more of the specified properties changed. + * @param props - Array of property names to check (e.g., ['position', 'size']) + * @returns Array of node IDs that have any of these properties modified by the initial state update or any previous middleware + */ + getAffectedNodeIds: (props: string[]) => string[]; + + /** + * Gets all edge IDs that have one or more of the specified properties changed. + * @param props - Array of property names to check (e.g., ['sourcePosition', 'targetPosition']) + * @returns Array of edge IDs that have any of these properties modified by the initial state update or any previous middleware + */ + getAffectedEdgeIds: (props: string[]) => string[]; + + /** + * Gets all nodes that were added. + * @returns Array of node instances that were added by the initial state update or any previous middleware + */ + getAddedNodes: () => Node[]; + + /** + * Gets all edges that were added. + * @returns Array of edge instances that were added by the initial state update or any previous middleware + */ + getAddedEdges: () => Edge[]; + + /** + * Gets all nodes that were removed. + * Uses `initialNodesMap` to access the removed instances. + * @returns Array of node instances that were removed by the initial state update or any previous middleware + */ + getRemovedNodes: () => Node[]; + + /** + * Gets all edges that were removed. + * Uses `initialEdgesMap` to access the removed instances. + * @returns Array of edge instances that were removed by the initial state update or any previous middleware + */ + getRemovedEdges: () => Edge[]; +} + +/** + * The context object passed to middleware execute functions. + * Provides access to the current state, helper functions, and configuration. + * + * @example + * ```typescript + * const middleware: Middleware = { + * name: 'validation', + * execute: (context, next, cancel) => { + * // Check if any nodes were added + * if (context.helpers.anyNodesAdded()) { + * console.log('Nodes added:', context.state.nodes); + * } + * + * // Access configuration + * console.log('Cell size:', context.config.background.cellSize); + * + * // Check what action triggered this + * if (context.modelActionType === 'addNodes') { + * // Validate new nodes + * const isValid = validateNodes(context.state.nodes); + * if (!isValid) { + * cancel(); // Block the operation + * return; + * } + * } + * + * next(); // Continue to next middleware + * } + * }; + * ``` * * @category Types */ export interface MiddlewareContext { - /** The initial state of the flow diagram */ + /** The state before any modifications (before the initial action and before any middleware modifications) */ initialState: FlowState; - /** The current state of the flow diagram */ + /** The current state (includes the initial modification and all changes from previous middlewares) */ state: FlowState; - /** A map of node IDs to their corresponding node objects */ + /** + * Map for quick node lookup by ID. + * Contains the current state after previous middleware processing. + * Use this to access nodes by ID instead of iterating through `state.nodes`. + */ nodesMap: Map; - /** A map of edge IDs to their corresponding edge objects*/ + /** + * Map for quick edge lookup by ID. + * Contains the current state after previous middleware processing. + * Use this to access edges by ID instead of iterating through `state.edges`. + */ edgesMap: Map; - /** The initial map of node IDs to their corresponding node objects */ + /** + * The initial nodes map before any modifications (before the initial action and before any middleware modifications). + * Use this to compare state before and after all modifications. + * Common usage: Access removed node instances that no longer exist in `nodesMap`. + */ initialNodesMap: Map; - /** The initial map of edge IDs to their corresponding edge objects */ + /** + * The initial edges map before any modifications (before the initial action and before any middleware modifications). + * Use this to compare state before and after all modifications. + * Common usage: Access removed edge instances that no longer exist in `edgesMap`. + */ initialEdgesMap: Map; - /** The type of action that triggered the middleware */ + /** The action that triggered the middleware execution */ modelActionType: ModelActionType; - /** The helper functions available to the middleware */ - helpers: ReturnType; - /** The history updates that have been applied so far */ + /** Helper functions to check what changed (tracks all cumulative changes from the initial action and all previous middlewares) */ + helpers: MiddlewareHelpers; + /** All state updates from previous middlewares in the chain */ history: MiddlewareHistoryUpdate[]; - /** The action state manager */ + /** Manager for action states (resizing, linking, etc.) */ actionStateManager: ActionStateManager; - /** The edge routing manager */ + /** Manager for edge routing algorithms */ edgeRoutingManager: EdgeRoutingManager; - /** The initial update to the flow state */ + /** + * The initial state update that triggered the middleware chain. + * Middlewares can add their own updates to the state, so this may not contain all modifications + * that will be applied. Use `helpers` to get actual knowledge about all changes. + */ initialUpdate: FlowStateUpdate; - /** The configuration for the flow diagram */ + /** The current diagram configuration */ config: FlowConfig; - /** The environment information */ + /** Environment information (browser, rendering engine, etc.) */ environment: EnvironmentInfo; } /** - * Type for middleware function that transforms state - * @template TMetadata - Type of the metadata of the middleware - * @template TName - Type of the name of the middleware (should be a string literal) + * Middleware interface for intercepting and modifying diagram state changes. * - * @category Types - */ - -/** - * Interface for middleware that can modify the flow state - * @template TName - Type of the name of the middleware (should be a string literal) + * Middlewares form a chain where each can: + * - Inspect the current state and action type + * - Modify the state by passing updates to `next()` + * - Block operations by calling `cancel()` + * - Perform side effects (logging, validation, etc.) + * + * @template TName - The middleware name type (string literal for type safety) + * + * @example + * ```typescript + * // Read-only middleware that blocks modifications + * const readOnlyMiddleware: Middleware<'read-only'> = { + * name: 'read-only', + * execute: (context, next, cancel) => { + * const blockedActions = ['addNodes', 'deleteNodes', 'updateNode']; + * if (blockedActions.includes(context.modelActionType)) { + * console.warn('Action blocked in read-only mode'); + * cancel(); + * return; + * } + * next(); + * } + * }; + * + * // Auto-snap middleware that modifies positions + * const snapMiddleware: Middleware<'auto-snap'> = { + * name: 'auto-snap', + * execute: (context, next) => { + * const gridSize = 20; + * const nodesToSnap = context.helpers.getAffectedNodeIds(['position']); + * + * const updates = nodesToSnap.map(id => { + * const node = context.nodesMap.get(id)!; + * return { + * id, + * position: { + * x: Math.round(node.position.x / gridSize) * gridSize, + * y: Math.round(node.position.y / gridSize) * gridSize + * } + * }; + * }); + * + * next({ nodesToUpdate: updates }); + * } + * }; + * + * // Register middleware + * ngDiagramService.registerMiddleware(snapMiddleware); + * ``` * * @category Types */ export interface Middleware { - /** The name of the middleware */ + /** Unique identifier for the middleware */ name: TName; - /** The function that executes the middleware logic */ + /** + * The middleware execution function. + * + * @param context - Complete context including state, helpers, and configuration + * @param next - Call this to continue to the next middleware (optionally with state updates) + * @param cancel - Call this to abort the entire operation + */ execute: ( - /** The context of the middleware */ context: MiddlewareContext, - /** Function to call to apply the state update and continue to the next middleware */ next: (stateUpdate?: FlowStateUpdate) => Promise, - /** Function to call to cancel the middleware execution */ cancel: () => void ) => Promise | void; } /** - * Type for middleware chain - * @template TState - Type of the state being modified + * An array of middlewares that will be executed in sequence. * * @category Types */ diff --git a/packages/ng-diagram/projects/ng-diagram/src/public-api.ts b/packages/ng-diagram/projects/ng-diagram/src/public-api.ts index cc5234adc..0680ee4dc 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/public-api.ts +++ b/packages/ng-diagram/projects/ng-diagram/src/public-api.ts @@ -69,6 +69,7 @@ export type { AppMiddlewares } from './lib/utils/create-middlewares'; // Core types re-export export type { ActionState, + ActionStateManager, BackgroundConfig, BoxSelectionConfig, ClipboardPastedEvent, @@ -81,9 +82,12 @@ export type { EdgeRouting, EdgeRoutingConfig, EdgeRoutingContext, + EdgeRoutingManager, EdgeRoutingName, EnvironmentInfo, FlowConfig, + FlowState, + FlowStateUpdate, GroupingConfig, GroupMembershipChangedEvent, GroupNode, @@ -98,7 +102,11 @@ export type { Metadata, Middleware, MiddlewareChain, + MiddlewareContext, + MiddlewareHelpers, + MiddlewareHistoryUpdate, Model, + ModelActionType, ModelAdapter, ModelChanges, ModifierOnlyShortcutBinding,