documentationfor yFiles for HTML 3.0.0.1

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.

Related events
Event Occurs when…​
drag-starting…​ a drag gesture is recognized, before the input mode is initialized for dragging.
query-position-handler…​ a drag gesture is recognized. Queries for a custom IPositionHandler. If the event arguments are not handled, the IPositionHandler in the positionHandler property is used.
drag-started…​ a drag gesture is recognized, after the input mode is initialized for dragging.
dragging…​ a mouse move event occurs during drag operations, before the actual drag is performed.
dragged…​ a mouse move event occurs during drag operations, after the actual drag has been performed.
drag-canceling…​ the drag gesture is canceled before the input mode has been cleaned up.
drag-canceled…​ the drag gesture is canceled after the input mode has been cleaned up.
drag-finishing…​ a drag gesture has ended successfully before the actual drag operations have been finished.
drag-finished…​ a drag gesture has ended successfully after the actual drag operations have been finished. At the time the event is raised, affectedItems are still valid.

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 cursor changes into a move cursor when hovering over a movable item
interaction move

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.

Register a custom position handler as wrapper
graph.decorator.nodes.positionHandler.addWrapperFactory(
  (_node, originalHandler) =>
    originalHandler != null
      ? new ConstrainedPositionHandler(originalHandler)
      : null
)
A constrained position handler wrapper
/**
 * 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.

Multiple candidate positions for moving a label
displaying the graph item layout labels interior

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.

Candidates
Move freely between candidates

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.

Always use a parameter finder
graph.decorator.labels.positionHandler.addFactory((label) => {
  const handler = new LabelPositionHandler(label)
  handler.useParameterFinderRecognizer = EventRecognizers.ALWAYS
  return handler
})