Import it from @grafloria/angular.
Card B — a floating, PATH-ANCHORED action layer.
Mirrors NodeToolbarComponent, but an edge has no bounding box to hang off: it is anchored to a FRACTION along the link (default the midpoint) and lifted off the stroke along the path NORMAL, so it never sits on top of the line it acts on. It re-anchors itself whenever the route changes — pan, zoom, a node drag that reroutes the edge, a waypoint edit.
Geometry comes from RenderedLinkPath (the drawn
ts@Component({
selector: 'grafloria-link-toolbar',
standalone: true,
imports: [CommonModule],
template: `
<div
#toolbar
class="grafloria-link-toolbar"
role="toolbar"
[attr.aria-label]="ariaLabel"
[attr.aria-hidden]="!isVisible"
[attr.data-link-id]="link?.id"
[class.visible]="isVisible"
[class.animated]="animation?.enabled !== false"
[style.transform]="transform"
[style.opacity]="isVisible ? 1 : 0"
[style.pointer-events]="isVisible ? 'all' : 'none'"
[style.--toolbar-bg]="styleConfig?.backgroundColor"
[style.--toolbar-border]="styleConfig?.borderColor"
[style.--toolbar-radius]="styleConfig?.borderRadius"
[style.--toolbar-shadow]="styleConfig?.boxShadow"
[style.--toolbar-padding]="styleConfig?.padding"
[style.--toolbar-z]="styleConfig?.zIndex"
[style.--toolbar-transition]="animation?.duration || '0.15s'"
(mouseenter)="onPointerEnter()"
(mouseleave)="onPointerLeave()"
(keydown)="handleKeyDown($event)"
>
@if (!customTemplate) {
<div class="toolbar-content" role="group">
@for (action of visibleActions; track action.id; let idx = $index) {
<button
type="button"
class="toolbar-button"
[attr.aria-label]="action.tooltip || action.label"
[attr.aria-disabled]="action.disabled"
[attr.data-action-id]="action.id"
[attr.tabindex]="idx === focusedActionIndex ? 0 : -1"
[disabled]="action.disabled"
[title]="action.tooltip || action.label"
(click)="handleActionClick(action, $event)"
(focus)="focusedActionIndex = idx"
>
@if (action.icon) {
<i [class]="action.icon" aria-hidden="true"></i>
}
<span>{{ action.label }}</span>
</button>
}
</div>
}
@if (customTemplate) {
<ng-container
*ngTemplateOutlet="customTemplate; context: { $implicit: link, actions: visibleActions, context: actionContext }">
</ng-container>
}
</div>
`,
styles: [`
.grafloria-link-toolbar {
position: absolute;
top: 0;
left: 0;
display: flex;
gap: 4px;
background: var(--toolbar-bg, var(--grafloria-toolbar-bg, white));
border: 1px solid var(--toolbar-border, var(--grafloria-toolbar-border, #e2e8f0));
border-radius: var(--toolbar-radius, var(--grafloria-toolbar-radius, 8px));
box-shadow: var(--toolbar-shadow, var(--grafloria-toolbar-shadow, 0 4px 6px rgba(0, 0, 0, 0.1)));
padding: var(--toolbar-padding, var(--grafloria-toolbar-padding, 2px));
z-index: var(--toolbar-z, var(--grafloria-toolbar-z, 1000));
outline: none;
white-space: nowrap;
}
.grafloria-link-toolbar.animated {
transition: opacity var(--toolbar-transition, 0.15s) ease;
}
.grafloria-link-toolbar:focus-within {
outline: 2px solid var(--grafloria-focus-color, #667eea);
outline-offset: 2px;
}
.toolbar-content {
display: flex;
gap: 2px;
}
.toolbar-button {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: transparent;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
line-height: 1.4;
color: var(--grafloria-toolbar-text, #334155);
transition: background-color 0.15s ease;
white-space: nowrap;
}
.toolbar-button:hover:not(:disabled) {
background: var(--grafloria-toolbar-hover, #f1f5f9);
}
.toolbar-button:active:not(:disabled) {
background: var(--grafloria-toolbar-active, #e2e8f0);
}
.toolbar-button:focus {
outline: 2px solid var(--grafloria-focus-color, #667eea);
outline-offset: -2px;
}
.toolbar-button:disabled,
…
Use it as <grafloria-link-toolbar> in a template.
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
link? | LinkModel | The link this toolbar acts on. | |
engine? | DiagramEngine | ||
canvasElement? | HTMLElement | Canvas container the toolbar is positioned inside (and measured against). | |
viewport? | { x: number; y: number; width: number; height: number } | Same viewport/zoom the canvas renders with — the screen-space fallback. | |
zoom? | |||
anchor? | Fraction along the path to glue the toolbar to (0 = source, 1 = target). | ||
offset? | Screen-px lift along the path normal, so the buttons clear the stroke. | ||
actions? | LinkToolbarAction[] | ||
customTemplate? | TemplateRef<any> | ||
visible? | |||
styleConfig? | ToolbarStyleConfig | ||
animation? | ToolbarAnimationConfig | ||
ariaLabel? |
Outputs
actionClickedpositionUpdatedpointerOverChange— Emits while the pointer is over the toolbar — lets the host keep it alive
Properties
| Name | Type | Default | Description |
|---|---|---|---|
link | LinkModel | The link this toolbar acts on. | |
engine | DiagramEngine | ||
canvasElement? | HTMLElement | Canvas container the toolbar is positioned inside (and measured against). | |
viewport | { x: number; y: number; width: number; height: number } | Same viewport/zoom the canvas renders with — the screen-space fallback. | |
zoom | 1.0 | ||
anchor | 0.5 | Fraction along the path to glue the toolbar to (0 = source, 1 = target). | |
offset | 22 | Screen-px lift along the path normal, so the buttons clear the stroke. | |
actions | LinkToolbarAction[] | [] | |
customTemplate? | TemplateRef<any> | ||
visible | true | ||
styleConfig? | ToolbarStyleConfig | ||
animation? | ToolbarAnimationConfig | ||
ariaLabel | 'Link actions' | ||
actionClicked | |||
positionUpdated | |||
pointerOverChange | Emits while the pointer is over the toolbar — lets the host keep it alive when the pointer leaves the link itself to reach for a button. | ||
toolbarRef? | ElementRef<HTMLDivElement> | ||
isVisible | false | ||
transform | '' | ||
focusedActionIndex | 0 |
Methods
constructor(private cdr: ChangeDetectorRef, private host: ElementRef<HTMLElement>)ngOnInit(): voidngOnChanges(changes: SimpleChanges): voidngOnDestroy(): voidget visibleActions(): LinkToolbarAction[]get actionContext(): LinkActionContext | null— What every button is handed: the link, WHERE on it, and in which direction.handleActionClick(action: LinkToolbarAction, event?: Event): voidhandleKeyDown(event: KeyboardEvent): voidonPointerEnter(): voidonPointerLeave(): voidupdatePosition(): void— Re-anchor to the CURRENT rendered route.
Runs the whole chain: rendered path → point + normal at anchor → screen
px → centred, normal-offset transform. Any missing link in that chain hides
the toolbar rather than parking it at (0, 0).
Example
html<grafloria-link-toolbar
[link]="hoveredLink" [engine]="engine"
[viewport]="viewport" [zoom]="zoom" [anchor]="0.5">
</grafloria-link-toolbar>
Was this page helpful?