// do not allow more than one label
graphEditorInputMode.addLabelPredicate = (item) =>
item instanceof ILabelOwner && item.labels.size < 1
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:
Furthermore, you can use the following two events on EditLabelInputMode to achieve results very similar to overriding the above methods:
Event | Occurs when … |
---|---|
… a label is about to be added. |
|
… 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 |
EditLabelInputMode offers a number of other events that allow you to react to certain stages in label editing:
Event | Occurs when … |
---|---|
… a label has been added. |
|
… the text editor is about to be opened for adding or editing a label. |
|
… a label has been edited successfully. |
|
… text editing has been canceled. |
|
… 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 |
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:
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:
Event | Occurs when … |
---|---|
… editing starts, but before the text area is displayed. |
|
… editing has been canceled. |
|
… 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 whenyfiles-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, whenyfiles-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 whenyfiles-labeleditbox-container-leave
is removed and it is removed when the CSS transition or animation ends. Thetransitionend
oranimationend
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.
graph.decorator.labels.editLabelHelper.addWrapperFactory(
(label, helper) =>
helper != null ? new WrappingEditLabelHelper(helper) : null
)
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.
graph.decorator.labels.editLabelHelper.addConstant(
new CustomEditLabelHelper()
)
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.