Moving Items
Moving graph items is handled by GraphEditorInputMode, which delegates most of the work to its child input modes moveUnselectedItemsInputMode for items in general and moveSelectedItemsInputMode for selected items in particular. Both input modes are enabled by default and must be disabled if only one kind of move interaction is desired. The types of items that can be moved are defined by the movableUnselectedItems and movableSelectedItems properties.
Common adjustments to item moving are as follows:
- GraphEditorInputMode.movableUnselectedItems
- GraphEditorInputMode.movableSelectedItems
- Specify the types of items that can be moved (unselected and selected, respectively).
- GraphEditorInputMode.movableUnselectedItemsPredicate
- GraphEditorInputMode.movableSelectedItemsPredicate
- A predicate to determine whether an item can be moved (unselected and selected, respectively).
- MoveInputMode.beginRecognizer
- Changes the mouse interaction that starts a move gesture.
- MoveInputMode.beginRecognizerTouch
- Changes the touch interaction that starts a move gesture. Note that for moveUnselectedItemsInputMode, this recognizer is set to NEVER, which means that, by default, unselected items cannot be moved with a touch gesture.
- MoveInputMode.validBeginCursor
- Changes the mouse cursor to display when hovering over a movable item.
- MoveInputMode.moveCursor
- Changes the mouse cursor to display during the move.
Moving items is handled by MoveInputMode. Some items, such as bends or ports, however, are instead moved with a special IHandle, managed by HandleInputMode. Note that customizing the results of the gesture works the same in either case, as described below.
The items involved in the move operation can be retrieved from property affectedItems. All events below are dispatched at a time when affectedItems are valid. Therefore, it is safe to access it in event handlers for these events to determine the moved items.
Event | Occurs when… |
---|---|
If you want to be informed about a successfully completed move operation, you can listen for drag-finished events.
When the mouse hovers over a movable item, the cursor changes to validBeginCursor to indicate this. Once moving the items is in progress, the cursor changes to moveCursor. By default, both are set to the MOVE cursor shape.

The actual moving of the graph items is performed by a position handler, modeled by implementations of the IPositionHandler interface. IPositionHandlers are similar to handles, and both actually inherit the same parent interface. The implementation of an IPositionHandler is responsible for actually moving the item.
The default IPositionHandler is a composite handler that collects all movable item’s position handlers (and handles) from the item’s lookup. Modifying the lookup to return a custom position handler can be a means to further customize the move behavior. For example, modifying a node’s lookup to hide the position handler prevents the node from being moved:
graph.decorator.nodes.positionHandler.hide(nodeShouldBeFixedPredicate)
In many cases, wrapping the existing implementation is a good way to customize movement of graph items, as shown below in Register a custom position handler as wrapper.
IPositionHandler uses four methods to maintain its life cycle:
- initializeDrag(context: IInputModeContext): void
- Initializes the position handler at the start of a drag operation. Implementers should allocate required resources, remember the initial position, and, if applicable, initialize snapping and orthogonal edge editing.
- handleMove(context: IInputModeContext, originalLocation: Point, newLocation: Point): void
- Called during a move gesture to update the current position.
- cancelDrag(context: IInputModeContext, originalLocation: Point): void
- Called when a move gesture is canceled. Implementers should restore the initial position and free all resources here.
- dragFinished(context: IInputModeContext, originalLocation: Point, newLocation: Point): void
- Called when a move gesture has finished successfully. Implementers should set the current position and free all resources here.
graph.decorator.nodes.positionHandler.addWrapperFactory(
(_node, originalHandler) =>
originalHandler != null
? new ConstrainedPositionHandler(originalHandler)
: null
)
/**
* A custom position handler which constrains the movement along the x-axis
* this implementation wraps the default position handler and delegates most of the work to it
*/
class ConstrainedPositionHandler extends BaseClass(IPositionHandler) {
handler
// remember the last location
lastLocation = null
// set the wrapped handler in the constructor
constructor(handler) {
super()
this.handler = handler
}
/**
* Interface implementation: delegate to wrapped handler
*/
get location() {
return this.handler.location
}
/**
* Initializes the drag
*/
initializeDrag(context) {
// delegate to the wrapped handler
this.handler.initializeDrag(context)
// remember the last location
this.lastLocation = this.handler.location.toPoint()
}
/**
* Handle the constrained move
*/
handleMove(context, originalLocation, newLocation) {
// only move along the x-axis, keep the original y coordinate
newLocation = new Point(newLocation.x, originalLocation.y)
if (!this.lastLocation || !newLocation.equalsEps(this.lastLocation, 0)) {
// delegate to the wrapped handler for the actual move
this.handler.handleMove(context, originalLocation, newLocation)
// remember the location
this.lastLocation = newLocation
}
}
cancelDrag(context, originalLocation) {
// drag has been cancelled: delegate to the wrapped handler
this.handler.cancelDrag(context, originalLocation)
}
dragFinished(context, originalLocation, newLocation) {
// drag has been successfully finished:
// delegate to the wrapped handler with the constrained position
this.handler.dragFinished(context, originalLocation, newLocation)
}
}
/**
* A custom position handler which constrains the movement along the x-axis
* this implementation wraps the default position handler and delegates most of the work to it
*/
class ConstrainedPositionHandler
extends BaseClass(IPositionHandler)
implements IPositionHandler
{
// remember the last location
private lastLocation: Point | null = null
// set the wrapped handler in the constructor
constructor(private readonly handler: IPositionHandler) {
super()
}
/**
* Interface implementation: delegate to wrapped handler
*/
get location(): IPoint {
return this.handler.location
}
/**
* Initializes the drag
*/
initializeDrag(context: IInputModeContext): void {
// delegate to the wrapped handler
this.handler.initializeDrag(context)
// remember the last location
this.lastLocation = this.handler.location.toPoint()
}
/**
* Handle the constrained move
*/
handleMove(
context: IInputModeContext,
originalLocation: Point,
newLocation: Point
): void {
// only move along the x-axis, keep the original y coordinate
newLocation = new Point(newLocation.x, originalLocation.y)
if (!this.lastLocation || !newLocation.equalsEps(this.lastLocation, 0)) {
// delegate to the wrapped handler for the actual move
this.handler.handleMove(context, originalLocation, newLocation)
// remember the location
this.lastLocation = newLocation
}
}
cancelDrag(context: IInputModeContext, originalLocation: Point): void {
// drag has been cancelled: delegate to the wrapped handler
this.handler.cancelDrag(context, originalLocation)
}
dragFinished(
context: IInputModeContext,
originalLocation: Point,
newLocation: Point
): void {
// drag has been successfully finished:
// delegate to the wrapped handler with the constrained position
this.handler.dragFinished(context, originalLocation, newLocation)
}
}
Customizing Grouping explains how moving nodes to a different parent node as part of a move gesture can be customized.
Moving Labels
Labels are typically constrained in their movement to specific positions determined by the label’s ILabelModel and ILabelModelParameter.
GraphEditorInputMode supports moving labels (like any other model item) via both moveUnselectedItemsInputMode and moveSelectedItemsInputMode. Class LabelPositionHandler, which is provided in a label’s lookup by default, is responsible for placing the labels according to their current ILabelModel.
There are different types of label models that behave slightly differently when labels are moved. Unrestricted label models like FreeNodeLabelModel or SmartEdgeLabelModel allow labels to be freely dragged to any position and support snapping interactions. On the other hand, models that allow only distinct positions, such as ExteriorNodeLabelModel or NinePositionsEdgeLabelModel, display a set of predetermined positions (candidates) to which labels can be moved.

Another type of label models restrict positions partially. For example, EdgeSegmentLabelModel only allows movements along the edge at a defined distance. These models also offer a set of candidate positions where labels can be placed. Additionally, by holding down the Ctrl key while dragging, labels can be moved freely within the supported constraints.


On a more technical level, when a label model’s lookup returns an ILabelModelParameterProvider, this instance is used to set the possible candidates. While holding down the Ctrl key, the label model’s lookup is queried for an ILabelModelParameterFinder. If one is returned, it determines the closest position supported by the model based on the given mouse coordinates. This feature allows the label to move freely along the allowed positions, such as moving at a defined distance to an edge.
To customize this behavior, you can set the LabelPositionHandler's useParameterFinderRecognizer to recognize a different modifier key. By setting the recognizer to ALWAYS, the ILabelModelParameterFinder is used whenever provided.
graph.decorator.labels.positionHandler.addFactory((label) => {
const handler = new LabelPositionHandler(label)
handler.useParameterFinderRecognizer = EventRecognizers.ALWAYS
return handler
})