Skip to content
D
Documentation

LinkToolbarComponent

reference
2 min readUpdated

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 , or the per-frame point polyline), never from LinkModel.segments — see that file for why.

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

NameTypeDefaultDescription
link?LinkModelThe link this toolbar acts on.
engine?DiagramEngine
canvasElement?HTMLElementCanvas 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

  • actionClicked
  • positionUpdated
  • pointerOverChange — Emits while the pointer is over the toolbar — lets the host keep it alive

Properties

NameTypeDefaultDescription
linkLinkModelThe link this toolbar acts on.
engineDiagramEngine
canvasElement?HTMLElementCanvas 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.
zoom1.0
anchor0.5Fraction along the path to glue the toolbar to (0 = source, 1 = target).
offset22Screen-px lift along the path normal, so the buttons clear the stroke.
actionsLinkToolbarAction[][]
customTemplate?TemplateRef<any>
visibletrue
styleConfig?ToolbarStyleConfig
animation?ToolbarAnimationConfig
ariaLabel'Link actions'
actionClicked
positionUpdated
pointerOverChangeEmits 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>
isVisiblefalse
transform''
focusedActionIndex0

Methods

  • constructor(private cdr: ChangeDetectorRef, private host: ElementRef<HTMLElement>)
  • ngOnInit(): void
  • ngOnChanges(changes: SimpleChanges): void
  • ngOnDestroy(): void
  • get 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): void
  • handleKeyDown(event: KeyboardEvent): void
  • onPointerEnter(): void
  • onPointerLeave(): void
  • updatePosition(): 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?

LinkToolbarComponent — Grafloria