documentationfor yFiles for HTML 3.0.0.3

Adding and Editing Labels

GraphEditorInputMode supports interactively adding and editing labels through its subordinate editLabelInputMode. The most prominent options to control label editing are GraphEditorInputMode's properties labelEditableItems, which define the item types that can be edited, and the boolean properties allowAddLabel and allowEditLabel, which globally allow or forbid adding and editing labels. These are described in the Adding and Editing Labels section.

For finer control over which items' labels can be added or edited, you can set custom addLabelPredicate and editLabelPredicate functions. For example, you could disallow adding or editing specific labels under certain circumstances. The following example shows how to ensure that label owners have no more than a single label by restricting label additions:

// do not allow more than one label
graphEditorInputMode.addLabelPredicate = (item) =>
  item instanceof ILabelOwner && item.labels.size < 1

Furthermore, you can use the following two events on EditLabelInputMode to achieve results very similar to overriding the above methods:

Customization events related to label adding and editing
Event Occurs when …​

query-label-adding

…​ a label is about to be added.

query-label-editing

…​ a label is about to be edited.

They allow you to change the label editing process completely by calling appropriate methods on their event argument. That argument, an LabelEditingEventArgs instance, offers various properties to customize editing labels. For example, to achieve the same result as the predicate above, you can also use the following event listener:

graphEditorInputMode.editLabelInputMode.addEventListener(
  'query-label-adding',
  (evt) => {
    if (evt.owner && evt.owner.labels.size >= 1) {
      evt.cancel = true
    }
  }
)

Apart from the cancel property, you can also set a different style, label layout parameter, or even redirect the editing process to a completely different label.

Tip

Both the customization events and the IEditLabelHelper's initialize method use the LabelEditingEventArgs for customization, essentially offering two redundant ways to customize. To streamline customization, it is recommended to primarily use implementations of IEditLabelHelper for item-wise customization. One example is the ChoreographyNodeStyle of the BPMN demo, which provides a customized IEditLabelHelper in its lookup method. On the other hand, application-wide settings might lean toward utilizing the events. As a general rule of thumb: the method that is easier to implement is the preferred choice.

EditLabelInputMode offers a number of other events that allow you to react to certain stages in label editing:

Related events
Event Occurs when …​

label-added

…​ a label has been added.

label-editing-started

…​ the text editor is about to be opened for adding or editing a label.

label-edited

…​ a label has been edited successfully.

label-editing-canceled

…​ text editing has been canceled.

validate-label-text

…​ text editing has been committed (usually by pressing Enter ↵). You can use this event to validate the text to apply to the new or edited label. You can set the validatedText property to null to prevent setting an invalid label text. The editor does not close when text validation fails. You can also change the text to a different value to implement validation that does not reject new values but rewrites them.

In addition to those events, GraphEditorInputMode provides label-added and label-edited events, which are raised after a label has been added or edited.

The query-label-adding and query-label-editing events offer quick and easy customization for all items. To change behavior only for certain label owners or labels, you can also decorate the lookup with a custom implementation of IEditLabelHelper. This interface defines two callback methods that are functionally identical to the query-label-adding and query-label-editing events on EditLabelInputMode.

Validating Label Text

The validate-label-text event can be used to validate the label text after the Enter key has been pressed. By default, if validation fails, the editor remains open, and editing is not stopped.

graphEditorInputMode.editLabelInputMode.addEventListener(
  'validate-label-text',
  (args, elim) => {
    // label must match an email address
    args.validatedText = isValidEmailAddress(args.newText)
      ? args.newText
      : null
  }
)

The Actual Text Editor: TextEditorInputMode

While the overall process of adding and editing labels is handled by EditLabelInputMode, the actual text editor is managed by TextEditorInputMode.

Most prominently, TextEditorInputMode displays a div element containing a textarea, which you can obtain from its editorContainer property and customize as necessary. For example, you can implement custom validation code that rejects typed characters during editing (as opposed to the validate-label-text event, which can only reject a label text after the user presses Enter ↵) by adding a suitable keypress to the container:

Customizing label editing by only allowing numeric input
const editorContainer =
  graphEditorInputMode.editLabelInputMode.textEditorInputMode
    .editorContainer
editorContainer.addEventListener('keypress', (e) => {
  if (isDigit(e.key)) {
    // prevent numeric input
    e.preventDefault()
  }
})

TextEditorInputMode also provides a number of events you can listen to:

Related events
Event Occurs when …​

editing-started

…​ editing starts, but before the text area is displayed.

editing-canceled

…​ editing has been canceled.

text-edited

…​ label editing has stopped, before the text area is hidden.

Placing the Text Box Individually

The TextEditorInputMode provides the properties location, anchor, and upVector to place the text box. During label editing, EditLabelInputMode alters these properties to place the text box over the currently edited label. Therefore, setting the positional properties beforehand has no effect.

The LabelEditingEventArgs used in EditLabelInputMode’s query-label-adding and query-label-editing events, as well as IEditLabelHelper’s methods, provides the textEditorInputModeConfigurator property which you can use to configure the TextEditorInputMode for a given label. The textEditorInputModeConfigurator has to be used to set the text area’s position since the position properties are overwritten by EditLabelInputMode during label editing, and the configurator is queried afterward:

graphEditorInputMode.editLabelInputMode.addEventListener(
  'query-label-editing',
  (args, elim) => {
    args.textEditorInputModeConfigurator = (
      context,
      inputMode,
      label
    ) => {
      // anchor the text editor centered above the label
      inputMode.anchor = new Point(0.5, 1)
      // place the editor always horizontal
      inputMode.upVector = new Point(0, -1)
      // place the editor relative to the label's center
      inputMode.location = label.layout.center
    }
  }
)

CSS Transition or Animation for the Input Element

The text input element uses different CSS classes when entering or leaving the DOM, allowing you to define CSS transitions or animations.

You can disable the transition or animation by not defining the related CSS classes described in the following sections.

Enter Phase

The following CSS classes are present when the input element is added to the DOM:

  • yfiles-labeleditbox-container-entering: This CSS class is present during the entire enter phase and can be used to define CSS transition or animation functions. It is removed when the enter transition or animation ends.

  • yfiles-labeleditbox-container-enter: Initializes the start state of the input element. The class is added before the element is inserted in the DOM and removed immediately after the element is added to the DOM.

  • yfiles-labeleditbox-container-enter-to: Defines the end state of the input element. This class is added when yfiles-labeleditbox-container-enter is removed (immediately after the element enters the DOM) and removed when the CSS transition or animation ends.

By default, yFiles provides a simple fade transition for the enter phase of the input element:

.yfiles-labeleditbox-container-enter {
   opacity: 0;
 }
.yfiles-labeleditbox-container-enter-to {
  opacity: 1;
}
.yfiles-labeleditbox-container-entering {
  transition: opacity 0.1s ease-in;
}

Alternatively, you could also define a CSS animation, for example:

@keyframes fade {
  from { opacity: 0 }
  to { opacity: 1 }
}
.yfiles-labeleditbox-container-entering {
  animation: fade 0.1s ease-in;
}

Leave Phase

The following CSS classes are present when the input element leaves the DOM:

  • yfiles-labeleditbox-container-leaving: This CSS class is present during the entire leave phase and can be used to define CSS transition or animation functions. It is removed when the enter transition or animation ends.

  • yfiles-labeleditbox-container-leave: Initializes the beginning of the leave state of the input element. The class is added when the leave phase begins and is removed immediately afterward, when yfiles-labeleditbox-container-leave-to is set.

  • yfiles-labeleditbox-container-leave-to: Defines the end state of the input element before it is removed from the DOM. This class is added when yfiles-labeleditbox-container-leave is removed and it is removed when the CSS transition or animation ends. The transitionend or animationend event also defines when the element is removed from the DOM.

By default, yFiles provides a simple fade transition for the leave phase of the input element:

.yfiles-labeleditbox-container-leave {
  opacity: 1;
}
.yfiles-labeleditbox-container-leave-to {
  opacity: 0;
}
.yfiles-labeleditbox-container-leaving {
  transition: opacity 0.1s ease-out;
}

Alternatively, you could also define a CSS animation, for example:

.yfiles-labeleditbox-container-leaving {
  animation: fade reverse 0.1s ease-out;
}

The IEditLabelHelper

An IEditLabelHelper controls editing existing labels and adding new labels to an ILabelOwner. The EditLabelInputMode tries to retrieve an instance of the IEditLabelHelper from the label or label owner’s lookup. All labels and label owners provide such an instance by default. Removing that instance from an element’s lookup prevents adding or editing labels.

graph.decorator.edges.editLabelHelper.hide()

The IEditLabelHelper undergoes a lifecycle: its initialize method is called at the start, and either its finish or cancel method is called at some point. This process allows maintaining a consistent state throughout the editing operation.

The initialize method utilizes the LabelEditingEventArgs, which are also used by the query-label-adding and query-label-editing events. Since these events are queried after the initialize method, values set here might be overridden in the respective event handlers. To ensure that the exact label or owner is edited or added with the provided properties, you need to set handled to true. If you set cancel to true, editing on a specific owner or label will be canceled.

initialize is called for both adding and editing labels. The LabelEditingAction indicates whether a label is being added or edited.

This example shows an implementation of initialize which supports adding no more than two labels to one node. Also, it sets the position of the first node to the center, whereas the second node is placed below the node:

initialize(
  context: IInputModeContext,
  evt: LabelEditingEventArgs,
  action: LabelEditingAction
): void {
  if (action == LabelEditingAction.ADD) {
    // support only two labels
    if (!(evt.owner instanceof INode) || evt.owner.labels.size >= 2) {
      evt.cancel = true
      return
    }

    evt.layoutParameter =
      evt.owner.labels.size == 0
        ? // first label in the center
          InteriorNodeLabelModel.CENTER
        : // second label north
          ExteriorNodeLabelModel.BOTTOM
    evt.style = context.graph!.nodeDefaults.labels.getStyleInstance(
      evt.owner
    )
  }
}

In the later stages of editing, the cancel function is invoked when the user cancels the editing operation or when another IEditLabelHelper takes control. If a state has been initialized in the initialize function, it needs to be properly reset or cleaned up at this point.

The finish method is invoked upon successful completion of label editing. This method is responsible for applying the changes based on the LabelEditingAction. If a new label needs to be added to the owner (provided in the arguments), the finish implementation should handle this task. In the case of editing an existing label, both the new values and the label itself are provided in the arguments and need to be updated, possibly including the label’s text, style, layoutParameter, and tag if applicable. Additionally, a label can be removed if the user clears the label text and autoRemoveEmptyLabels is set to true.

It is the IEditLabelHelper's task to provide meaningful defaults upon initialization and to apply the editing results in the finish method. Usually, one does not need to fully customize all these actions. Therefore, it is suggested to write an implementation which wraps the default implementation.

Apply a wrapping implementation
graph.decorator.labels.editLabelHelper.addWrapperFactory(
  (label, helper) =>
    helper != null ? new WrappingEditLabelHelper(helper) : null
)
Sample implementation which provides a custom parameter for newly added edge labels
class WrappingEditLabelHelper
  extends BaseClass(IEditLabelHelper)
  implements IEditLabelHelper
{
  wrapped: IEditLabelHelper

  constructor(wrapped: IEditLabelHelper) {
    super()
    this.wrapped = wrapped
  }

  initialize(
    context: IInputModeContext,
    args: LabelEditingEventArgs,
    action: LabelEditingAction
  ): void {
    // let the wrapped implementation do the work
    this.wrapped.initialize(context, args, action)
    // for new edge labels: use a custom parameter
    if (action == LabelEditingAction.ADD && args.owner instanceof IEdge) {
      args.layoutParameter =
        new EdgeSegmentLabelModel().createParameterFromCenter()
    }
  }

  // Finish completely delegates to the wrapped implementation
  finish(
    context: IInputModeContext,
    evt: LabelEditingEventArgs,
    action: LabelEditingAction | LabelEditingActionStringValues
  ): ILabel | null {
    return this.wrapped.finish(context, evt, action)
  }

  // Cancel completely delegates to the wrapped implementation
  cancel(context: IInputModeContext, evt: LabelEditingEventArgs): void {
    return this.wrapped.cancel(context, evt)
  }
}

Alternatively, one can extend the EditLabelHelper class, which offers a range of overridable methods to fine-tune control while retaining the core functionality.

Apply a constant implementation
graph.decorator.labels.editLabelHelper.addConstant(
  new CustomEditLabelHelper()
)
Sample implementation which provides a custom parameter for newly added edge labels
class CustomEditLabelHelper extends EditLabelHelper {
  protected onLabelAdding(
    context: IInputModeContext,
    evt: LabelEditingEventArgs
  ) {
    // let the base implementation do the work
    super.onLabelAdding(context, evt)
    // for new edge labels: use a custom parameter
    if (evt.owner instanceof IEdge) {
      evt.layoutParameter =
        new EdgeSegmentLabelModel().createParameterFromCenter()
    }
  }
}

Note that the IEditLabelHelper interface is also utilized in other input gestures. For instance, the GraphClipboard uses the helper for the PASTE action when pasting labels. Similarly, the LabelDropInputMode queries the helper for the DROP action when labels are dropped over an owner.