From 21ed232173e754ddabbcff2f70efbd64dce3c49c Mon Sep 17 00:00:00 2001 From: Piotr Blaszczyk Date: Thu, 22 Jan 2026 10:54:50 +0100 Subject: [PATCH 1/6] [AF-250] Add minimap component for diagram overview and navigation - Add NgDiagramMinimapComponent displaying bird's-eye view of the diagram - Add NgDiagramMinimapNavigationDirective for click-and-drag panning - Add MinimapProviderService for minimap availability management - Add NgDiagramPanelPosition type for positioning panels - Add minimap calculations for coordinate transformations - Update watermark component to use panel positioning - Add CSS tokens and styles for minimap theming - Add API documentation for new components - Integrate minimap in angular-demo app --- apps/angular-demo/src/app/app.component.html | 1 + apps/angular-demo/src/app/app.component.ts | 9 +- .../Components/NgDiagramMinimapComponent.md | 44 ++++++ .../NgDiagramMinimapNavigationDirective.md | 31 +++++ .../docs/api/Types/NgDiagramPanelPosition.md | 11 ++ apps/docs/src/content/docs/api/_readme.md | 6 + ...ng-diagram-minimap-navigation.directive.ts | 79 +++++++++++ .../ng-diagram-minimap.calculations.ts | 125 ++++++++++++++++++ .../minimap/ng-diagram-minimap.component.html | 36 +++++ .../minimap/ng-diagram-minimap.component.scss | 56 ++++++++ .../minimap/ng-diagram-minimap.component.ts | 105 +++++++++++++++ .../minimap/ng-diagram-minimap.types.ts | 30 +++++ .../watermark/watermark.component.scss | 22 ++- .../watermark/watermark.component.ts | 26 +++- .../src/lib/providers/ng-diagram.providers.ts | 2 + .../minimap-provider.service.ts | 21 +++ .../ng-diagram/src/lib/styles/primitives.css | 2 + .../ng-diagram/src/lib/styles/styles.css | 13 ++ .../ng-diagram/src/lib/styles/tokens.css | 6 + .../src/lib/types/panel-position.ts | 8 ++ .../projects/ng-diagram/src/public-api.ts | 3 + 21 files changed, 631 insertions(+), 5 deletions(-) create mode 100644 apps/docs/src/content/docs/api/Components/NgDiagramMinimapComponent.md create mode 100644 apps/docs/src/content/docs/api/Directives/NgDiagramMinimapNavigationDirective.md create mode 100644 apps/docs/src/content/docs/api/Types/NgDiagramPanelPosition.md create mode 100644 packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap-navigation.directive.ts create mode 100644 packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.calculations.ts create mode 100644 packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.html create mode 100644 packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.scss create mode 100644 packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.ts create mode 100644 packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.types.ts create mode 100644 packages/ng-diagram/projects/ng-diagram/src/lib/services/minimap-provider/minimap-provider.service.ts create mode 100644 packages/ng-diagram/projects/ng-diagram/src/lib/types/panel-position.ts diff --git a/apps/angular-demo/src/app/app.component.html b/apps/angular-demo/src/app/app.component.html index d0a2c4876..e6cc9ead7 100644 --- a/apps/angular-demo/src/app/app.component.html +++ b/apps/angular-demo/src/app/app.component.html @@ -18,6 +18,7 @@ > + diff --git a/apps/angular-demo/src/app/app.component.ts b/apps/angular-demo/src/app/app.component.ts index 8fbf2e458..3752fda33 100644 --- a/apps/angular-demo/src/app/app.component.ts +++ b/apps/angular-demo/src/app/app.component.ts @@ -10,6 +10,7 @@ import { NgDiagramComponent, NgDiagramConfig, NgDiagramEdgeTemplateMap, + NgDiagramMinimapComponent, NgDiagramNodeTemplateMap, NgDiagramPaletteItem, NodeResizedEvent, @@ -39,7 +40,13 @@ import { ToolbarComponent } from './toolbar/toolbar.component'; selector: 'app-root', templateUrl: './app.component.html', styleUrl: './app.component.scss', - imports: [ToolbarComponent, PaletteComponent, NgDiagramComponent, NgDiagramBackgroundComponent], + imports: [ + ToolbarComponent, + PaletteComponent, + NgDiagramComponent, + NgDiagramBackgroundComponent, + NgDiagramMinimapComponent, + ], providers: [provideNgDiagram()], changeDetection: ChangeDetectionStrategy.OnPush, }) diff --git a/apps/docs/src/content/docs/api/Components/NgDiagramMinimapComponent.md b/apps/docs/src/content/docs/api/Components/NgDiagramMinimapComponent.md new file mode 100644 index 000000000..8265bbd80 --- /dev/null +++ b/apps/docs/src/content/docs/api/Components/NgDiagramMinimapComponent.md @@ -0,0 +1,44 @@ +--- +version: "since v0.9.1" +editUrl: false +next: false +prev: false +title: "NgDiagramMinimapComponent" +--- + +A minimap component that displays a bird's-eye view of the diagram. + +Shows all nodes as small rectangles and a viewport rectangle indicating +the currently visible area. The minimap updates reactively when the +diagram viewport changes (pan/zoom) or when nodes are added/removed/updated. + +The minimap also supports navigation: click and drag on the minimap to pan +the diagram viewport to different areas. + +## Implements + +- `OnDestroy` + +## Properties + +### height + +> **height**: `InputSignal`\<`number`\> + +Height of the minimap in pixels. + +*** + +### position + +> **position**: `InputSignal`\<[`NgDiagramPanelPosition`](/docs/api/types/ngdiagrampanelposition/)\> + +Position of the minimap panel within the diagram container. + +*** + +### width + +> **width**: `InputSignal`\<`number`\> + +Width of the minimap in pixels. diff --git a/apps/docs/src/content/docs/api/Directives/NgDiagramMinimapNavigationDirective.md b/apps/docs/src/content/docs/api/Directives/NgDiagramMinimapNavigationDirective.md new file mode 100644 index 000000000..ba4b74fb9 --- /dev/null +++ b/apps/docs/src/content/docs/api/Directives/NgDiagramMinimapNavigationDirective.md @@ -0,0 +1,31 @@ +--- +version: "since v0.9.1" +editUrl: false +next: false +prev: false +title: "NgDiagramMinimapNavigationDirective" +--- + +Directive that enables drag navigation on the minimap. +Users can drag on the minimap to move the diagram viewport. + +## Implements + +- `OnDestroy` + +## Methods + +### ngOnDestroy() + +> **ngOnDestroy**(): `void` + +A callback method that performs custom clean-up, invoked immediately +before a directive, pipe, or service instance is destroyed. + +#### Returns + +`void` + +#### Implementation of + +`OnDestroy.ngOnDestroy` diff --git a/apps/docs/src/content/docs/api/Types/NgDiagramPanelPosition.md b/apps/docs/src/content/docs/api/Types/NgDiagramPanelPosition.md new file mode 100644 index 000000000..a8068b1bc --- /dev/null +++ b/apps/docs/src/content/docs/api/Types/NgDiagramPanelPosition.md @@ -0,0 +1,11 @@ +--- +version: "since v0.9.1" +editUrl: false +next: false +prev: false +title: "NgDiagramPanelPosition" +--- + +> **NgDiagramPanelPosition** = `"top-left"` \| `"top-right"` \| `"bottom-left"` \| `"bottom-right"` + +Position for diagram overlay panels (minimap, watermark, etc.). diff --git a/apps/docs/src/content/docs/api/_readme.md b/apps/docs/src/content/docs/api/_readme.md index da8bc80ab..4160ac307 100644 --- a/apps/docs/src/content/docs/api/_readme.md +++ b/apps/docs/src/content/docs/api/_readme.md @@ -13,6 +13,7 @@ title: "ng-diagram" - [NgDiagramBaseNodeTemplateComponent](/docs/api/components/ngdiagrambasenodetemplatecomponent/) - [NgDiagramComponent](/docs/api/components/ngdiagramcomponent/) - [NgDiagramMarkerComponent](/docs/api/components/ngdiagrammarkercomponent/) +- [NgDiagramMinimapComponent](/docs/api/components/ngdiagramminimapcomponent/) - [NgDiagramNodeResizeAdornmentComponent](/docs/api/components/ngdiagramnoderesizeadornmentcomponent/) - [NgDiagramNodeRotateAdornmentComponent](/docs/api/components/ngdiagramnoderotateadornmentcomponent/) - [NgDiagramPaletteItemComponent](/docs/api/components/ngdiagrampaletteitemcomponent/) @@ -22,6 +23,7 @@ title: "ng-diagram" ## Directives - [NgDiagramGroupHighlightedDirective](/docs/api/directives/ngdiagramgrouphighlighteddirective/) +- [NgDiagramMinimapNavigationDirective](/docs/api/directives/ngdiagramminimapnavigationdirective/) - [NgDiagramNodeSelectedDirective](/docs/api/directives/ngdiagramnodeselecteddirective/) ## Internals @@ -51,6 +53,10 @@ title: "ng-diagram" - [NgDiagramService](/docs/api/services/ngdiagramservice/) - [NgDiagramViewportService](/docs/api/services/ngdiagramviewportservice/) +## Types + +- [NgDiagramPanelPosition](/docs/api/types/ngdiagrampanelposition/) + ## Types/Configuration - [FlowConfig](/docs/api/types/configuration/flowconfig/) diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap-navigation.directive.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap-navigation.directive.ts new file mode 100644 index 000000000..aae2ee8d4 --- /dev/null +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap-navigation.directive.ts @@ -0,0 +1,79 @@ +import { Directive, inject, input, OnDestroy } from '@angular/core'; +import { Viewport } from '../../../core/src'; +import { NgDiagramViewportService } from '../../public-services/ng-diagram-viewport.service'; +import { MinimapTransform } from './ng-diagram-minimap.types'; + +/** + * Directive that enables drag navigation on the minimap. + * Users can drag on the minimap to move the diagram viewport. + * + * @public + * @since 0.9.1 + * @category Directives + */ +@Directive({ + selector: '[ngDiagramMinimapNavigation]', + standalone: true, + host: { + '(pointerdown)': 'onPointerDown($event)', + }, +}) +export class NgDiagramMinimapNavigationDirective implements OnDestroy { + private readonly viewportService = inject(NgDiagramViewportService); + + transform = input.required(); + viewport = input.required(); + + private isDragging = false; + private lastClientX = 0; + private lastClientY = 0; + + ngOnDestroy(): void { + this.removeDocumentListeners(); + } + + onPointerDown(event: PointerEvent): void { + if (event.button !== 0) { + return; + } + + event.preventDefault(); + + this.isDragging = true; + this.lastClientX = event.clientX; + this.lastClientY = event.clientY; + + document.addEventListener('pointermove', this.onPointerMove); + document.addEventListener('pointerup', this.onPointerUp); + } + + private onPointerMove = (event: PointerEvent): void => { + if (!this.isDragging) { + return; + } + + const deltaX = event.clientX - this.lastClientX; + const deltaY = event.clientY - this.lastClientY; + + this.lastClientX = event.clientX; + this.lastClientY = event.clientY; + + const transform = this.transform(); + const viewport = this.viewport(); + + const diagramDeltaX = deltaX / transform.scale; + const diagramDeltaY = deltaY / transform.scale; + + this.viewportService.moveViewportBy(-diagramDeltaX * viewport.scale, -diagramDeltaY * viewport.scale); + }; + + private onPointerUp = (): void => { + this.isDragging = false; + this.removeDocumentListeners(); + }; + + private removeDocumentListeners(): void { + document.removeEventListener('pointermove', this.onPointerMove); + document.removeEventListener('pointerup', this.onPointerUp); + } +} diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.calculations.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.calculations.ts new file mode 100644 index 000000000..2ae6110ba --- /dev/null +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.calculations.ts @@ -0,0 +1,125 @@ +import { Node, Rect, unionRect } from '../../../core/src'; +import { MinimapNode, MinimapTransform, MinimapViewportRect } from './ng-diagram-minimap.types'; + +/** + * Converts viewport position and scale to a bounding rectangle in diagram coordinate space. + * This represents the visible area of the diagram. + */ +export const convertViewportToDiagramBounds = (viewport: { + x: number; + y: number; + scale: number; + width?: number; + height?: number; +}): Rect => { + const containerWidth = viewport.width ?? 0; + const containerHeight = viewport.height ?? 0; + + return { + x: -viewport.x / viewport.scale, + y: -viewport.y / viewport.scale, + width: containerWidth / viewport.scale, + height: containerHeight / viewport.scale, + }; +}; + +/** + * Creates a union of diagram content bounds and viewport bounds. + * This ensures the minimap shows both the diagram content and the current view position. + */ +export const combineDiagramAndViewportBounds = (diagramBounds: Rect, viewportBounds: Rect): Rect => { + if (viewportBounds.width === 0 || viewportBounds.height === 0) { + return diagramBounds; + } + return unionRect([diagramBounds, viewportBounds]); +}; + +/** + * Calculates the scale factor needed to fit bounds within the available minimap space. + */ +const calculateScaleToFitBounds = ( + bounds: Rect, + minimapWidth: number, + minimapHeight: number, + padding: number +): number => { + const availableWidth = minimapWidth - padding * 2; + const availableHeight = minimapHeight - padding * 2; + + if (bounds.width === 0 || bounds.height === 0) { + return 1; + } + + return Math.min(availableWidth / bounds.width, availableHeight / bounds.height); +}; + +/** + * Calculates the X and Y offsets needed to center the scaled content within the minimap. + */ +const calculateCenteringOffset = ( + bounds: Rect, + minimapWidth: number, + minimapHeight: number, + scale: number +): { offsetX: number; offsetY: number } => { + const scaledWidth = bounds.width * scale; + const scaledHeight = bounds.height * scale; + + return { + offsetX: (minimapWidth - scaledWidth) / 2 - bounds.x * scale, + offsetY: (minimapHeight - scaledHeight) / 2 - bounds.y * scale, + }; +}; + +/** + * Computes the complete minimap transform (scale and offset) to fit bounds within minimap dimensions. + */ +export const calculateMinimapTransform = ( + bounds: Rect, + minimapWidth: number, + minimapHeight: number, + padding: number +): MinimapTransform => { + const scale = calculateScaleToFitBounds(bounds, minimapWidth, minimapHeight, padding); + const { offsetX, offsetY } = calculateCenteringOffset(bounds, minimapWidth, minimapHeight, scale); + return { scale, offsetX, offsetY }; +}; + +/** + * Transforms a diagram node to minimap coordinate space. + */ +export const transformNodeToMinimapSpace = (node: Node, transform: MinimapTransform): MinimapNode => { + const size = node.size ?? { width: 100, height: 50 }; + + return { + id: node.id, + x: node.position.x * transform.scale + transform.offsetX, + y: node.position.y * transform.scale + transform.offsetY, + width: size.width * transform.scale, + height: size.height * transform.scale, + angle: node.angle ?? 0, + }; +}; + +/** + * Transforms the viewport rectangle to minimap coordinate space. + */ +export const transformViewportToMinimapSpace = ( + viewport: { x: number; y: number; scale: number; width?: number; height?: number }, + transform: MinimapTransform +): MinimapViewportRect => { + const containerWidth = viewport.width ?? 0; + const containerHeight = viewport.height ?? 0; + + const visibleX = -viewport.x / viewport.scale; + const visibleY = -viewport.y / viewport.scale; + const visibleWidth = containerWidth / viewport.scale; + const visibleHeight = containerHeight / viewport.scale; + + return { + x: visibleX * transform.scale + transform.offsetX, + y: visibleY * transform.scale + transform.offsetY, + width: visibleWidth * transform.scale, + height: visibleHeight * transform.scale, + }; +}; diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.html b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.html new file mode 100644 index 000000000..044118269 --- /dev/null +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.html @@ -0,0 +1,36 @@ +
+ + + @if (isDiagramInitialized()) { + @for (node of minimapNodes(); track node.id) { + + } + @if (hasValidViewport()) { + + } + } + +
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.scss b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.scss new file mode 100644 index 000000000..cedb0e850 --- /dev/null +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.scss @@ -0,0 +1,56 @@ +:host { + display: block; + position: absolute; + pointer-events: auto; + z-index: 10; + + &.bottom-right { + bottom: var(--ngd-minimap-margin); + right: var(--ngd-minimap-margin); + } + + &.bottom-left { + bottom: var(--ngd-minimap-margin); + left: var(--ngd-minimap-margin); + } + + &.top-right { + top: var(--ngd-minimap-margin); + right: var(--ngd-minimap-margin); + } + + &.top-left { + top: var(--ngd-minimap-margin); + left: var(--ngd-minimap-margin); + } +} + +.minimap-container { + overflow: hidden; + border-radius: var(--ngd-minimap-border-radius); + padding: var(--ngd-minimap-padding); + background-color: var(--ngd-minimap-background); + box-shadow: 0 8px 16px -4px var(--ngd-minimap-shadow-color); + border: 1px solid var(--ngd-minimap-border-color); +} + +.minimap-svg { + display: block; + cursor: pointer; +} + +.minimap-background { + fill: transparent; +} + +.minimap-node { + fill: var(--ngd-minimap-node-color); + opacity: var(--ngd-minimap-node-opacity); +} + +.minimap-viewport { + stroke: var(--ngd-minimap-viewport-stroke-color); + stroke-width: var(--ngd-minimap-viewport-stroke-width); + fill: transparent; + pointer-events: none; +} diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.ts new file mode 100644 index 000000000..e89f5ab99 --- /dev/null +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.ts @@ -0,0 +1,105 @@ +import { ChangeDetectionStrategy, Component, computed, ElementRef, inject, input, OnDestroy } from '@angular/core'; +import { Node } from '../../../core/src'; +import { NgDiagramModelService } from '../../public-services/ng-diagram-model.service'; +import { MinimapProviderService } from '../../services/minimap-provider/minimap-provider.service'; +import { RendererService } from '../../services/renderer/renderer.service'; +import { NgDiagramPanelPosition } from '../../types/panel-position'; +import { NgDiagramMinimapNavigationDirective } from './ng-diagram-minimap-navigation.directive'; +import { + calculateMinimapTransform, + combineDiagramAndViewportBounds, + convertViewportToDiagramBounds, + transformNodeToMinimapSpace, + transformViewportToMinimapSpace, +} from './ng-diagram-minimap.calculations'; + +/** + * A minimap component that displays a bird's-eye view of the diagram. + * + * Shows all nodes as small rectangles and a viewport rectangle indicating + * the currently visible area. The minimap updates reactively when the + * diagram viewport changes (pan/zoom) or when nodes are added/removed/updated. + * + * The minimap also supports navigation: click and drag on the minimap to pan + * the diagram viewport to different areas. + * + * @public + * @since 0.9.1 + * @category Components + */ +@Component({ + selector: 'ng-diagram-minimap', + standalone: true, + imports: [NgDiagramMinimapNavigationDirective], + templateUrl: './ng-diagram-minimap.component.html', + styleUrls: ['./ng-diagram-minimap.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class]': 'position()', + }, +}) +export class NgDiagramMinimapComponent implements OnDestroy { + private readonly VIEWPORT_STROKE_WIDTH_CSS_VAR = '--ngd-minimap-viewport-stroke-width'; + + private readonly modelService = inject(NgDiagramModelService); + private readonly renderer = inject(RendererService); + private readonly elementRef = inject(ElementRef); + private readonly minimapProviderService = inject(MinimapProviderService); + + /** Position of the minimap panel within the diagram container. */ + position = input('bottom-right'); + + /** Width of the minimap in pixels. */ + width = input(200); + + /** Height of the minimap in pixels. */ + height = input(150); + + constructor() { + this.minimapProviderService.register(this); + } + + /** @ignore */ + ngOnDestroy(): void { + this.minimapProviderService.unregister(); + } + + isDiagramInitialized = this.renderer.isInitialized; + nodes = this.renderer.nodes; + viewport = this.renderer.viewport; + + diagramBounds = computed(() => { + const nodes = this.nodes(); + return this.modelService.computePartsBounds(nodes, []); + }); + + transform = computed(() => + calculateMinimapTransform(this.combinedBounds(), this.width(), this.height(), this.getStrokePadding()) + ); + + minimapNodes = computed(() => this.nodes().map((node: Node) => transformNodeToMinimapSpace(node, this.transform()))); + + hasValidViewport = computed(() => { + const viewport = this.viewport(); + return !!viewport.width && !!viewport.height && viewport.width > 0 && viewport.height > 0; + }); + + viewportRect = computed(() => transformViewportToMinimapSpace(this.viewport(), this.transform())); + + private viewportBoundsInDiagramSpace = computed(() => convertViewportToDiagramBounds(this.viewport())); + + private combinedBounds = computed(() => + combineDiagramAndViewportBounds(this.diagramBounds(), this.viewportBoundsInDiagramSpace()) + ); + + /** + * Reads viewport stroke width from CSS to use as internal padding. + * This prevents the viewport rectangle stroke from being clipped at minimap edges. + */ + private getStrokePadding(): number { + const style = getComputedStyle(this.elementRef.nativeElement); + const strokeWidth = style.getPropertyValue(this.VIEWPORT_STROKE_WIDTH_CSS_VAR).trim(); + + return parseFloat(strokeWidth) || 1; + } +} diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.types.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.types.ts new file mode 100644 index 000000000..b5a6c114f --- /dev/null +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.types.ts @@ -0,0 +1,30 @@ +/** + * Represents the calculated transform data for minimap rendering. + */ +export interface MinimapTransform { + scale: number; + offsetX: number; + offsetY: number; +} + +/** + * Represents a node's visual representation in the minimap. + */ +export interface MinimapNode { + id: string; + x: number; + y: number; + width: number; + height: number; + angle: number; +} + +/** + * Represents the viewport rectangle in minimap space. + */ +export interface MinimapViewportRect { + x: number; + y: number; + width: number; + height: number; +} diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/watermark/watermark.component.scss b/packages/ng-diagram/projects/ng-diagram/src/lib/components/watermark/watermark.component.scss index 0e4e265d4..fac9cbede 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/watermark/watermark.component.scss +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/watermark/watermark.component.scss @@ -1,9 +1,27 @@ :host { position: absolute; - bottom: 1rem; - right: 1rem; width: var(--watermark-size); + &.bottom-right { + bottom: 1rem; + right: 1rem; + } + + &.top-right { + top: 1rem; + right: 1rem; + } + + &.bottom-left { + bottom: 1rem; + left: 1rem; + } + + &.top-left { + top: 1rem; + left: 1rem; + } + svg { opacity: 0.7; height: 100%; diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/watermark/watermark.component.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/watermark/watermark.component.ts index 188e9f128..1b353e082 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/watermark/watermark.component.ts +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/watermark/watermark.component.ts @@ -1,9 +1,31 @@ -import { Component } from '@angular/core'; +import { Component, computed, inject } from '@angular/core'; +import { MinimapProviderService } from '../../services/minimap-provider/minimap-provider.service'; +import { NgDiagramPanelPosition } from '../../types/panel-position'; @Component({ selector: 'ng-diagram-watermark', standalone: true, templateUrl: './watermark.component.html', styleUrls: ['./watermark.component.scss'], + host: { + '[class]': 'position()', + }, }) -export class NgDiagramWatermarkComponent {} +export class NgDiagramWatermarkComponent { + private readonly DEFAULT_POSITION: NgDiagramPanelPosition = 'bottom-right'; + private readonly ALTERNATIVE_POSITION: NgDiagramPanelPosition = 'top-right'; + + private readonly minimapProviderService = inject(MinimapProviderService); + + /** + * Computes the watermark position based on minimap position. + * If minimap is at the same position, watermark moves to avoid overlap. + */ + position = computed(() => { + if (this.minimapProviderService.position() === this.DEFAULT_POSITION) { + return this.ALTERNATIVE_POSITION; + } + + return this.DEFAULT_POSITION; + }); +} diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/providers/ng-diagram.providers.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/providers/ng-diagram.providers.ts index 25c5b76ea..6d8e0cbfd 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/providers/ng-diagram.providers.ts +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/providers/ng-diagram.providers.ts @@ -14,6 +14,7 @@ import { InputEventsRouterService } from '../services/input-events/input-events- import { LinkingEventService } from '../services/input-events/linking-event.service'; import { ManualLinkingService } from '../services/input-events/manual-linking.service'; import { MarkerRegistryService } from '../services/marker-registry/marker-registry.service'; +import { MinimapProviderService } from '../services/minimap-provider/minimap-provider.service'; import { PaletteService } from '../services/palette/palette.service'; import { RendererService } from '../services/renderer/renderer.service'; import { TemplateProviderService } from '../services/template-provider/template-provider.service'; @@ -69,5 +70,6 @@ export function provideNgDiagram(): Provider[] { ManualLinkingService, TemplateProviderService, MarkerRegistryService, + MinimapProviderService, ]; } diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/services/minimap-provider/minimap-provider.service.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/services/minimap-provider/minimap-provider.service.ts new file mode 100644 index 000000000..bd908d5b4 --- /dev/null +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/services/minimap-provider/minimap-provider.service.ts @@ -0,0 +1,21 @@ +import { computed, Injectable, signal } from '@angular/core'; +import type { NgDiagramMinimapComponent } from '../../components/minimap/ng-diagram-minimap.component'; + +/** + * Service that provides access to the registered minimap component. + * Used by other components (like watermark) to coordinate their positioning. + */ +@Injectable() +export class MinimapProviderService { + private readonly minimap = signal(null); + + position = computed(() => this.minimap()?.position()); + + register(component: NgDiagramMinimapComponent): void { + this.minimap.set(component); + } + + unregister(): void { + this.minimap.set(null); + } +} diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/styles/primitives.css b/packages/ng-diagram/projects/ng-diagram/src/lib/styles/primitives.css index dff957d17..fad6909e3 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/styles/primitives.css +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/styles/primitives.css @@ -13,6 +13,8 @@ --ngd-colors-gray-650: #383a40; --ngd-colors-gray-700: #27282b; --ngd-colors-gray-800: #151516; + --ngd-colors-gray-900-20: rgba(7, 7, 8, 0.2); + --ngd-colors-gray-900-50: rgba(7, 7, 8, 0.5); --ngd-colors-acc1-400: #a977ff; --ngd-colors-acc1-500: #9140ff; --ngd-colors-acc1-500-40: rgba(145, 64, 255, 0.4); diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/styles/styles.css b/packages/ng-diagram/projects/ng-diagram/src/lib/styles/styles.css index 5ddf65896..2f4e52915 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/styles/styles.css +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/styles/styles.css @@ -64,6 +64,19 @@ --ngd-background-line-major-width: 1; --ngd-background-line-minor-opacity: 0.5; --ngd-background-line-major-opacity: 0.6; + + --ngd-minimap-background: var(--ngd-minimap-bg-primary-default); + --ngd-minimap-border-color: var(--ngd-minimap-stroke-primary-default); + --ngd-minimap-shadow-color: var(--ngd-shadow); + --ngd-minimap-border-radius: 1rem; + --ngd-minimap-padding: 0.5rem; + --ngd-minimap-margin: 1rem; + + --ngd-minimap-node-color: var(--ngd-colors-gray-400); + --ngd-minimap-node-opacity: 0.8; + + --ngd-minimap-viewport-stroke-color: var(--ngd-colors-gray-400); + --ngd-minimap-viewport-stroke-width: 1; } .ng-diagram-port-hoverable .ng-diagram-port:hover, diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/styles/tokens.css b/packages/ng-diagram/projects/ng-diagram/src/lib/styles/tokens.css index ef98e54b6..da4125d8b 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/styles/tokens.css +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/styles/tokens.css @@ -2,6 +2,7 @@ /* DEFAULT LIGHT THEME */ :root { + --ngd-shadow: var(--ngd-colors-gray-900-20); --ngd-txt-primary-default: var(--ngd-colors-gray-800); --ngd-diagram-background-color: var(--ngd-colors-gray-200); --ngd-ui-bg-primary-default: var(--ngd-colors-gray-100); @@ -24,10 +25,13 @@ --ngd-background-line-minor-color: var(--ngd-colors-gray-450); --ngd-background-line-major-color: var(--ngd-colors-gray-500); --ngd-ui-border-color: var(--ngd-colors-gray-300); + --ngd-minimap-bg-primary-default: var(--ngd-colors-gray-100); + --ngd-minimap-stroke-primary-default: var(--ngd-colors-gray-400); } /* OPTIONAL DARK THEME */ html[data-theme='dark'] { + --ngd-shadow: var(--ngd-colors-gray-900-50); --ngd-txt-primary-default: var(--ngd-colors-gray-100); --ngd-diagram-background-color: var(--ngd-colors-gray-800); --ngd-ui-bg-primary-default: var(--ngd-colors-gray-700); @@ -50,4 +54,6 @@ html[data-theme='dark'] { --ngd-background-line-minor-color: var(--ngd-colors-gray-700); --ngd-background-line-major-color: var(--ngd-colors-gray-600); --ngd-ui-border-color: var(--ngd-colors-gray-700); + --ngd-minimap-bg-primary-default: var(--ngd-colors-gray-700); + --ngd-minimap-stroke-primary-default: var(--ngd-colors-gray-600); } diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/types/panel-position.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/types/panel-position.ts new file mode 100644 index 000000000..57c9fd749 --- /dev/null +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/types/panel-position.ts @@ -0,0 +1,8 @@ +/** + * Position for diagram overlay panels (minimap, watermark, etc.). + * + * @public + * @since 0.9.1 + * @category Types + */ +export type NgDiagramPanelPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; diff --git a/packages/ng-diagram/projects/ng-diagram/src/public-api.ts b/packages/ng-diagram/projects/ng-diagram/src/public-api.ts index 6fc0bfc9e..37ef9e081 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/public-api.ts +++ b/packages/ng-diagram/projects/ng-diagram/src/public-api.ts @@ -11,6 +11,7 @@ export { } from './lib/components/edge-label/base-edge-label/base-edge-label.component'; export { NgDiagramBaseEdgeComponent } from './lib/components/edge/base-edge/base-edge.component'; export { NgDiagramMarkerComponent } from './lib/components/marker/ng-diagram-marker.component'; +export { NgDiagramMinimapComponent } from './lib/components/minimap/ng-diagram-minimap.component'; export { NgDiagramBaseNodeTemplateComponent } from './lib/components/node/base-node-template/ng-diagram-base-node-template.component'; export { NgDiagramNodeResizeAdornmentComponent } from './lib/components/node/resize/ng-diagram-node-resize-adornment.component'; export { NgDiagramNodeRotateAdornmentComponent } from './lib/components/node/rotate/ng-diagram-node-rotate-adornment.component'; @@ -41,6 +42,7 @@ export { ViewportDirective } from './lib/directives/viewport/viewport.directive' export { ZIndexDirective } from './lib/directives/z-index/z-index.directive'; // Public directives +export { NgDiagramMinimapNavigationDirective } from './lib/components/minimap/ng-diagram-minimap-navigation.directive'; export { NgDiagramGroupHighlightedDirective } from './lib/directives/group-highlighted/ng-diagram-group-highlighted.directive'; export { NgDiagramNodeSelectedDirective } from './lib/directives/node-selected/ng-diagram-node-selected.directive'; @@ -67,6 +69,7 @@ export type { NgDiagramEdgeTemplate } from './lib/types/edge-template-map'; export type { PointerInputEvent } from './lib/types/event'; export type { NgDiagramGroupNodeTemplate, NgDiagramNodeTemplate } from './lib/types/node-template-map'; export type { BasePaletteItemData, GroupNodeData, NgDiagramPaletteItem, SimpleNodeData } from './lib/types/palette'; +export type { NgDiagramPanelPosition } from './lib/types/panel-position'; export type { AppMiddlewares } from './lib/utils/create-middlewares'; // Core types re-export From 4b18088ce059f7247c4a8c95384e97297f115a64 Mon Sep 17 00:00:00 2001 From: Piotr Blaszczyk Date: Tue, 27 Jan 2026 13:57:42 +0100 Subject: [PATCH 2/6] add possibility to provide custom styles and templates for the minimap --- apps/angular-demo/src/app/app.component.html | 2 +- apps/angular-demo/src/app/app.component.ts | 20 ++ .../image-minimap-node.component.scss | 10 + .../image-minimap-node.component.ts | 19 ++ .../Components/NgDiagramMinimapComponent.md | 42 +++- .../NgDiagramMinimapNavigationDirective.md | 2 +- .../api/Types/Minimap/MinimapNodeShape.md | 11 + .../api/Types/Minimap/MinimapNodeStyle.md | 58 +++++ .../api/Types/Minimap/MinimapNodeStyleFn.md | 31 +++ .../Minimap/NgDiagramMinimapNodeTemplate.md | 49 ++++ .../NgDiagramMinimapNodeTemplateMap.md | 224 ++++++++++++++++++ .../docs/api/Types/NgDiagramPanelPosition.md | 2 +- apps/docs/src/content/docs/api/_readme.md | 8 + ...iagram-default-minimap-node.component.html | 16 ++ ...iagram-default-minimap-node.component.scss | 4 + ...-diagram-default-minimap-node.component.ts | 46 ++++ ...ng-diagram-minimap-navigation.directive.ts | 2 +- .../ng-diagram-minimap.calculations.ts | 35 ++- .../minimap/ng-diagram-minimap.component.html | 29 ++- .../minimap/ng-diagram-minimap.component.scss | 5 - .../minimap/ng-diagram-minimap.component.ts | 118 +++++++-- .../minimap/ng-diagram-minimap.types.ts | 131 +++++++++- .../src/lib/types/panel-position.ts | 2 +- .../projects/ng-diagram/src/public-api.ts | 7 + 24 files changed, 822 insertions(+), 51 deletions(-) create mode 100644 apps/angular-demo/src/app/minimap-node-template/image-minimap-node/image-minimap-node.component.scss create mode 100644 apps/angular-demo/src/app/minimap-node-template/image-minimap-node/image-minimap-node.component.ts create mode 100644 apps/docs/src/content/docs/api/Types/Minimap/MinimapNodeShape.md create mode 100644 apps/docs/src/content/docs/api/Types/Minimap/MinimapNodeStyle.md create mode 100644 apps/docs/src/content/docs/api/Types/Minimap/MinimapNodeStyleFn.md create mode 100644 apps/docs/src/content/docs/api/Types/Minimap/NgDiagramMinimapNodeTemplate.md create mode 100644 apps/docs/src/content/docs/api/Types/Minimap/NgDiagramMinimapNodeTemplateMap.md create mode 100644 packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/default-node/ng-diagram-default-minimap-node.component.html create mode 100644 packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/default-node/ng-diagram-default-minimap-node.component.scss create mode 100644 packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/default-node/ng-diagram-default-minimap-node.component.ts diff --git a/apps/angular-demo/src/app/app.component.html b/apps/angular-demo/src/app/app.component.html index e6cc9ead7..180828c10 100644 --- a/apps/angular-demo/src/app/app.component.html +++ b/apps/angular-demo/src/app/app.component.html @@ -18,7 +18,7 @@ > - + diff --git a/apps/angular-demo/src/app/app.component.ts b/apps/angular-demo/src/app/app.component.ts index 3752fda33..865924d2d 100644 --- a/apps/angular-demo/src/app/app.component.ts +++ b/apps/angular-demo/src/app/app.component.ts @@ -6,11 +6,13 @@ import { EdgeDrawnEvent, GroupMembershipChangedEvent, initializeModel, + MinimapNodeStyle, NgDiagramBackgroundComponent, NgDiagramComponent, NgDiagramConfig, NgDiagramEdgeTemplateMap, NgDiagramMinimapComponent, + NgDiagramMinimapNodeTemplateMap, NgDiagramNodeTemplateMap, NgDiagramPaletteItem, NodeResizedEvent, @@ -33,6 +35,7 @@ import { ButtonEdgeComponent } from './edge-template/button-edge/button-edge.com import { CustomPolylineEdgeComponent } from './edge-template/custom-polyline-edge/custom-polyline-edge.component'; import { DashedEdgeComponent } from './edge-template/dashed-edge/dashed-edge.component'; import { LabelledEdgeComponent } from './edge-template/labelled-edge/labelled-edge.component'; +import { ImageMinimapNodeComponent } from './minimap-node-template/image-minimap-node/image-minimap-node.component'; import { PaletteComponent } from './palette/palette.component'; import { ToolbarComponent } from './toolbar/toolbar.component'; @@ -62,6 +65,8 @@ export class AppComponent { ['dashed-edge', DashedEdgeComponent], ]); + minimapNodeTemplateMap = new NgDiagramMinimapNodeTemplateMap([['image', ImageMinimapNodeComponent]]); + config = { zoom: { max: 2, @@ -212,4 +217,19 @@ export class AppComponent { } model = initializeModel(defaultModel); + + nodeStyle(node: Node): MinimapNodeStyle { + const style: MinimapNodeStyle = {}; + + if (node.id == '13') { + style.shape = 'circle'; + } + + if (node.selected) { + style.stroke = 'var(--ngd-node-stroke-primary-hover)'; + style.strokeWidth = 5; + } + + return style; + } } diff --git a/apps/angular-demo/src/app/minimap-node-template/image-minimap-node/image-minimap-node.component.scss b/apps/angular-demo/src/app/minimap-node-template/image-minimap-node/image-minimap-node.component.scss new file mode 100644 index 000000000..f21203ff4 --- /dev/null +++ b/apps/angular-demo/src/app/minimap-node-template/image-minimap-node/image-minimap-node.component.scss @@ -0,0 +1,10 @@ +:host { + display: contents; + + img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + } +} diff --git a/apps/angular-demo/src/app/minimap-node-template/image-minimap-node/image-minimap-node.component.ts b/apps/angular-demo/src/app/minimap-node-template/image-minimap-node/image-minimap-node.component.ts new file mode 100644 index 000000000..bff529ab8 --- /dev/null +++ b/apps/angular-demo/src/app/minimap-node-template/image-minimap-node/image-minimap-node.component.ts @@ -0,0 +1,19 @@ +import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; +import { MinimapNodeStyle, NgDiagramMinimapNodeTemplate, type Node } from 'ng-diagram'; + +@Component({ + selector: 'app-image-minimap-node', + standalone: true, + template: `image`, + styleUrl: './image-minimap-node.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ImageMinimapNodeComponent implements NgDiagramMinimapNodeTemplate { + node = input.required(); + nodeStyle = input(); + + imageUrl = computed(() => { + const data = this.node().data as { imageUrl?: string } | undefined; + return data?.imageUrl ?? 'https://placehold.jp/150x150.png'; + }); +} diff --git a/apps/docs/src/content/docs/api/Components/NgDiagramMinimapComponent.md b/apps/docs/src/content/docs/api/Components/NgDiagramMinimapComponent.md index 8265bbd80..e31cc0123 100644 --- a/apps/docs/src/content/docs/api/Components/NgDiagramMinimapComponent.md +++ b/apps/docs/src/content/docs/api/Components/NgDiagramMinimapComponent.md @@ -1,5 +1,5 @@ --- -version: "since v0.9.1" +version: "since v1.0.0" editUrl: false next: false prev: false @@ -17,6 +17,7 @@ the diagram viewport to different areas. ## Implements +- `AfterViewInit` - `OnDestroy` ## Properties @@ -29,6 +30,45 @@ Height of the minimap in pixels. *** +### minimapNodeTemplateMap + +> **minimapNodeTemplateMap**: `InputSignal`\<[`NgDiagramMinimapNodeTemplateMap`](/docs/api/types/minimap/ngdiagramminimapnodetemplatemap/)\> + +Optional template map for complete control over node rendering per node type. +Components registered in the map should render SVG elements. + +#### Example + +```typescript +const minimapTemplateMap = new NgDiagramMinimapNodeTemplateMap([ + ['database', DatabaseMinimapNodeComponent], + ['api', ApiMinimapNodeComponent], +]); + +// Usage: + +``` + +*** + +### nodeStyle + +> **nodeStyle**: `InputSignal`\<`undefined` \| [`MinimapNodeStyleFn`](/docs/api/types/minimap/minimapnodestylefn/)\> + +Optional callback function to customize node styling. +Return style properties to override defaults, or null/undefined to use CSS defaults. + +#### Example + +```typescript +nodeStyle = (node: Node) => ({ + fill: node.type === 'database' ? '#4CAF50' : '#9E9E9E', + opacity: node.selected ? 1 : 0.6, +}); +``` + +*** + ### position > **position**: `InputSignal`\<[`NgDiagramPanelPosition`](/docs/api/types/ngdiagrampanelposition/)\> diff --git a/apps/docs/src/content/docs/api/Directives/NgDiagramMinimapNavigationDirective.md b/apps/docs/src/content/docs/api/Directives/NgDiagramMinimapNavigationDirective.md index ba4b74fb9..a67ec05ad 100644 --- a/apps/docs/src/content/docs/api/Directives/NgDiagramMinimapNavigationDirective.md +++ b/apps/docs/src/content/docs/api/Directives/NgDiagramMinimapNavigationDirective.md @@ -1,5 +1,5 @@ --- -version: "since v0.9.1" +version: "since v1.0.0" editUrl: false next: false prev: false diff --git a/apps/docs/src/content/docs/api/Types/Minimap/MinimapNodeShape.md b/apps/docs/src/content/docs/api/Types/Minimap/MinimapNodeShape.md new file mode 100644 index 000000000..00b0f93c8 --- /dev/null +++ b/apps/docs/src/content/docs/api/Types/Minimap/MinimapNodeShape.md @@ -0,0 +1,11 @@ +--- +version: "since v1.0.0" +editUrl: false +next: false +prev: false +title: "MinimapNodeShape" +--- + +> **MinimapNodeShape** = `"rect"` \| `"circle"` \| `"ellipse"` + +Available shapes for minimap node rendering. diff --git a/apps/docs/src/content/docs/api/Types/Minimap/MinimapNodeStyle.md b/apps/docs/src/content/docs/api/Types/Minimap/MinimapNodeStyle.md new file mode 100644 index 000000000..82441499a --- /dev/null +++ b/apps/docs/src/content/docs/api/Types/Minimap/MinimapNodeStyle.md @@ -0,0 +1,58 @@ +--- +version: "since v1.0.0" +editUrl: false +next: false +prev: false +title: "MinimapNodeStyle" +--- + +Style properties that can be applied to minimap nodes. +All properties are optional - unset properties use CSS defaults. + +## Properties + +### cssClass? + +> `optional` **cssClass**: `string` + +CSS class to apply to the node + +*** + +### fill? + +> `optional` **fill**: `string` + +Fill color for the node + +*** + +### opacity? + +> `optional` **opacity**: `number` + +Opacity from 0 to 1 + +*** + +### shape? + +> `optional` **shape**: [`MinimapNodeShape`](/docs/api/types/minimap/minimapnodeshape/) + +Shape of the node in the minimap. Defaults to 'rect'. + +*** + +### stroke? + +> `optional` **stroke**: `string` + +Stroke color for the node + +*** + +### strokeWidth? + +> `optional` **strokeWidth**: `number` + +Stroke width in pixels diff --git a/apps/docs/src/content/docs/api/Types/Minimap/MinimapNodeStyleFn.md b/apps/docs/src/content/docs/api/Types/Minimap/MinimapNodeStyleFn.md new file mode 100644 index 000000000..167bd7f67 --- /dev/null +++ b/apps/docs/src/content/docs/api/Types/Minimap/MinimapNodeStyleFn.md @@ -0,0 +1,31 @@ +--- +version: "since v1.0.0" +editUrl: false +next: false +prev: false +title: "MinimapNodeStyleFn" +--- + +> **MinimapNodeStyleFn** = (`node`) => [`MinimapNodeStyle`](/docs/api/types/minimap/minimapnodestyle/) \| `null` \| `undefined` + +Function signature for the nodeStyle callback. +Return style properties to override defaults, or null/undefined to use defaults. + +## Parameters + +### node + +[`Node`](/docs/api/types/model/node/) + +## Returns + +[`MinimapNodeStyle`](/docs/api/types/minimap/minimapnodestyle/) \| `null` \| `undefined` + +## Example + +```typescript +const nodeStyle: MinimapNodeStyleFn = (node) => ({ + fill: node.type === 'database' ? '#4CAF50' : '#9E9E9E', + opacity: node.selected ? 1 : 0.6, +}); +``` diff --git a/apps/docs/src/content/docs/api/Types/Minimap/NgDiagramMinimapNodeTemplate.md b/apps/docs/src/content/docs/api/Types/Minimap/NgDiagramMinimapNodeTemplate.md new file mode 100644 index 000000000..a74a7139e --- /dev/null +++ b/apps/docs/src/content/docs/api/Types/Minimap/NgDiagramMinimapNodeTemplate.md @@ -0,0 +1,49 @@ +--- +version: "since v1.0.0" +editUrl: false +next: false +prev: false +title: "NgDiagramMinimapNodeTemplate" +--- + +Interface for custom minimap node components. +Components implementing this interface can be registered in NgDiagramMinimapNodeTemplateMap +to customize how specific node types are rendered in the minimap. + +Custom templates are rendered inside a foreignObject that handles positioning and sizing, +so the component only needs to render content that fills its container. + +## Example + +```typescript +@Component({ + selector: 'my-minimap-node', + standalone: true, + template: ` +
+ {{ node().type }} +
+ `, + styles: [`.minimap-icon { width: 100%; height: 100%; }`] +}) +export class MyMinimapNodeComponent implements NgDiagramMinimapNodeTemplate { + node = input.required(); + nodeStyle = input(); // Required by interface, can be ignored if not needed +} +``` + +## Properties + +### node + +> **node**: `InputSignal`\<[`Node`](/docs/api/types/model/node/)\> + +Input signal containing the original Node object for accessing node data, type, etc. + +*** + +### nodeStyle + +> **nodeStyle**: `InputSignal`\<`undefined` \| [`MinimapNodeStyle`](/docs/api/types/minimap/minimapnodestyle/)\> + +Input signal for style overrides computed by nodeStyle callback. Can be ignored if not needed. diff --git a/apps/docs/src/content/docs/api/Types/Minimap/NgDiagramMinimapNodeTemplateMap.md b/apps/docs/src/content/docs/api/Types/Minimap/NgDiagramMinimapNodeTemplateMap.md new file mode 100644 index 000000000..1e46dac41 --- /dev/null +++ b/apps/docs/src/content/docs/api/Types/Minimap/NgDiagramMinimapNodeTemplateMap.md @@ -0,0 +1,224 @@ +--- +version: "since v1.0.0" +editUrl: false +next: false +prev: false +title: "NgDiagramMinimapNodeTemplateMap" +--- + +Map that associates node type names with their corresponding minimap Angular component classes. +Used by ng-diagram-minimap to determine which custom component to render based on node type. + +## Example + +```typescript +const minimapTemplateMap = new NgDiagramMinimapNodeTemplateMap([ + ['database', DatabaseMinimapNodeComponent], + ['api', ApiMinimapNodeComponent], +]); + +// Usage in template: + +``` + +## Extends + +- `Map`\<`string`, `Type`\<[`NgDiagramMinimapNodeTemplate`](/docs/api/types/minimap/ngdiagramminimapnodetemplate/)\>\> + +## Properties + +### size + +> `readonly` **size**: `number` + +#### Returns + +the number of elements in the Map. + +#### Inherited from + +`Map.size` + +## Methods + +### \[iterator\]() + +> **\[iterator\]**(): `MapIterator`\<\[`string`, `Type$1`\<[`NgDiagramMinimapNodeTemplate`](/docs/api/types/minimap/ngdiagramminimapnodetemplate/)\>\]\> + +Returns an iterable of entries in the map. + +#### Returns + +`MapIterator`\<\[`string`, `Type$1`\<[`NgDiagramMinimapNodeTemplate`](/docs/api/types/minimap/ngdiagramminimapnodetemplate/)\>\]\> + +#### Inherited from + +`Map.[iterator]` + +*** + +### delete() + +> **delete**(`key`): `boolean` + +#### Parameters + +##### key + +`string` + +#### Returns + +`boolean` + +true if an element in the Map existed and has been removed, or false if the element does not exist. + +#### Inherited from + +`Map.delete` + +*** + +### entries() + +> **entries**(): `MapIterator`\<\[`string`, `Type$1`\<[`NgDiagramMinimapNodeTemplate`](/docs/api/types/minimap/ngdiagramminimapnodetemplate/)\>\]\> + +Returns an iterable of key, value pairs for every entry in the map. + +#### Returns + +`MapIterator`\<\[`string`, `Type$1`\<[`NgDiagramMinimapNodeTemplate`](/docs/api/types/minimap/ngdiagramminimapnodetemplate/)\>\]\> + +#### Inherited from + +`Map.entries` + +*** + +### forEach() + +> **forEach**(`callbackfn`, `thisArg?`): `void` + +Executes a provided function once per each key/value pair in the Map, in insertion order. + +#### Parameters + +##### callbackfn + +(`value`, `key`, `map`) => `void` + +##### thisArg? + +`any` + +#### Returns + +`void` + +#### Inherited from + +`Map.forEach` + +*** + +### get() + +> **get**(`key`): `undefined` \| `Type$1`\<[`NgDiagramMinimapNodeTemplate`](/docs/api/types/minimap/ngdiagramminimapnodetemplate/)\> + +Returns a specified element from the Map object. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map. + +#### Parameters + +##### key + +`string` + +#### Returns + +`undefined` \| `Type$1`\<[`NgDiagramMinimapNodeTemplate`](/docs/api/types/minimap/ngdiagramminimapnodetemplate/)\> + +Returns the element associated with the specified key. If no element is associated with the specified key, undefined is returned. + +#### Inherited from + +`Map.get` + +*** + +### has() + +> **has**(`key`): `boolean` + +#### Parameters + +##### key + +`string` + +#### Returns + +`boolean` + +boolean indicating whether an element with the specified key exists or not. + +#### Inherited from + +`Map.has` + +*** + +### keys() + +> **keys**(): `MapIterator`\<`string`\> + +Returns an iterable of keys in the map + +#### Returns + +`MapIterator`\<`string`\> + +#### Inherited from + +`Map.keys` + +*** + +### set() + +> **set**(`key`, `value`): `this` + +Adds a new element with a specified key and value to the Map. If an element with the same key already exists, the element will be updated. + +#### Parameters + +##### key + +`string` + +##### value + +`Type$1` + +#### Returns + +`this` + +#### Inherited from + +`Map.set` + +*** + +### values() + +> **values**(): `MapIterator`\<`Type$1`\<[`NgDiagramMinimapNodeTemplate`](/docs/api/types/minimap/ngdiagramminimapnodetemplate/)\>\> + +Returns an iterable of values in the map + +#### Returns + +`MapIterator`\<`Type$1`\<[`NgDiagramMinimapNodeTemplate`](/docs/api/types/minimap/ngdiagramminimapnodetemplate/)\>\> + +#### Inherited from + +`Map.values` diff --git a/apps/docs/src/content/docs/api/Types/NgDiagramPanelPosition.md b/apps/docs/src/content/docs/api/Types/NgDiagramPanelPosition.md index a8068b1bc..c6c472bac 100644 --- a/apps/docs/src/content/docs/api/Types/NgDiagramPanelPosition.md +++ b/apps/docs/src/content/docs/api/Types/NgDiagramPanelPosition.md @@ -1,5 +1,5 @@ --- -version: "since v0.9.1" +version: "since v1.0.0" editUrl: false next: false prev: false diff --git a/apps/docs/src/content/docs/api/_readme.md b/apps/docs/src/content/docs/api/_readme.md index 4160ac307..e4607b270 100644 --- a/apps/docs/src/content/docs/api/_readme.md +++ b/apps/docs/src/content/docs/api/_readme.md @@ -125,6 +125,14 @@ title: "ng-diagram" - [ModelActionType](/docs/api/types/middleware/modelactiontype/) - [ModelActionTypes](/docs/api/types/middleware/modelactiontypes/) +## Types/Minimap + +- [NgDiagramMinimapNodeTemplateMap](/docs/api/types/minimap/ngdiagramminimapnodetemplatemap/) +- [MinimapNodeStyle](/docs/api/types/minimap/minimapnodestyle/) +- [NgDiagramMinimapNodeTemplate](/docs/api/types/minimap/ngdiagramminimapnodetemplate/) +- [MinimapNodeShape](/docs/api/types/minimap/minimapnodeshape/) +- [MinimapNodeStyleFn](/docs/api/types/minimap/minimapnodestylefn/) + ## Types/Model - [Edge](/docs/api/types/model/edge/) diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/default-node/ng-diagram-default-minimap-node.component.html b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/default-node/ng-diagram-default-minimap-node.component.html new file mode 100644 index 000000000..9964aeb42 --- /dev/null +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/default-node/ng-diagram-default-minimap-node.component.html @@ -0,0 +1,16 @@ +@switch (shape()) { + @case ('circle') { + + } + @case ('ellipse') { + + } + @default { + + } +} diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/default-node/ng-diagram-default-minimap-node.component.scss b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/default-node/ng-diagram-default-minimap-node.component.scss new file mode 100644 index 000000000..09a410c7b --- /dev/null +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/default-node/ng-diagram-default-minimap-node.component.scss @@ -0,0 +1,4 @@ +:host { + fill: var(--ngd-minimap-node-color); + opacity: var(--ngd-minimap-node-opacity); +} diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/default-node/ng-diagram-default-minimap-node.component.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/default-node/ng-diagram-default-minimap-node.component.ts new file mode 100644 index 000000000..7bad259f9 --- /dev/null +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/default-node/ng-diagram-default-minimap-node.component.ts @@ -0,0 +1,46 @@ +/* eslint-disable @angular-eslint/component-selector */ +import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; +import { MinimapBounds, MinimapNodeStyle } from '../ng-diagram-minimap.types'; + +/** + * Internal default component for rendering minimap nodes as SVG shapes. + * Renders directly in SVG context (not inside foreignObject) using + * pre-transformed minimap coordinates. + * + * @internal + */ + +@Component({ + selector: 'g[ng-diagram-default-minimap-node]', + standalone: true, + templateUrl: './ng-diagram-default-minimap-node.component.html', + styleUrl: './ng-diagram-default-minimap-node.component.scss', + host: { + '[class]': 'hostClass()', + '[attr.transform]': 'bounds().transform', + '[style.fill]': 'computedStyle().fill', + '[style.stroke]': 'computedStyle().stroke', + '[style.stroke-width.px]': 'computedStyle().strokeWidth', + '[style.opacity]': 'computedStyle().opacity', + }, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NgDiagramDefaultMinimapNodeComponent { + bounds = input.required(); + nodeStyle = input(); + + computedStyle = computed(() => this.nodeStyle() ?? {}); + shape = computed(() => this.computedStyle().shape ?? 'rect'); + + /** Host class combining base class with optional custom cssClass. */ + hostClass = computed(() => { + const cssClass = this.computedStyle().cssClass; + return cssClass ? `minimap-node ${cssClass}` : 'minimap-node'; + }); + + cx = computed(() => this.bounds().x + this.bounds().width / 2); + cy = computed(() => this.bounds().y + this.bounds().height / 2); + radius = computed(() => Math.min(this.bounds().width, this.bounds().height) / 2); + rx = computed(() => this.bounds().width / 2); + ry = computed(() => this.bounds().height / 2); +} diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap-navigation.directive.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap-navigation.directive.ts index aae2ee8d4..9758002a7 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap-navigation.directive.ts +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap-navigation.directive.ts @@ -8,7 +8,7 @@ import { MinimapTransform } from './ng-diagram-minimap.types'; * Users can drag on the minimap to move the diagram viewport. * * @public - * @since 0.9.1 + * @since 1.0.0 * @category Directives */ @Directive({ diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.calculations.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.calculations.ts index 2ae6110ba..82bb90b7c 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.calculations.ts +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.calculations.ts @@ -1,5 +1,5 @@ import { Node, Rect, unionRect } from '../../../core/src'; -import { MinimapNode, MinimapTransform, MinimapViewportRect } from './ng-diagram-minimap.types'; +import { MinimapBounds, MinimapTransform, MinimapViewportRect } from './ng-diagram-minimap.types'; /** * Converts viewport position and scale to a bounding rectangle in diagram coordinate space. @@ -85,19 +85,36 @@ export const calculateMinimapTransform = ( return { scale, offsetX, offsetY }; }; +/** Default node size when not specified. */ +const DEFAULT_NODE_SIZE = { width: 100, height: 50 }; + /** - * Transforms a diagram node to minimap coordinate space. + * Extracts node bounds in diagram coordinate space. + * Used with SVG group transform - the group applies scale/offset, + * so individual nodes stay in their original diagram coordinates. + * + * @param node - The diagram node + * @param defaultSize - Optional default size for nodes without explicit size */ -export const transformNodeToMinimapSpace = (node: Node, transform: MinimapTransform): MinimapNode => { - const size = node.size ?? { width: 100, height: 50 }; +export const extractNodeBounds = ( + node: Node, + defaultSize: { width: number; height: number } = DEFAULT_NODE_SIZE +): MinimapBounds => { + const size = node.size ?? defaultSize; + const angle = node.angle ?? 0; + + // Calculate rotation center in diagram space + const centerX = node.position.x + size.width / 2; + const centerY = node.position.y + size.height / 2; return { id: node.id, - x: node.position.x * transform.scale + transform.offsetX, - y: node.position.y * transform.scale + transform.offsetY, - width: size.width * transform.scale, - height: size.height * transform.scale, - angle: node.angle ?? 0, + x: node.position.x, + y: node.position.y, + width: size.width, + height: size.height, + angle, + transform: `rotate(${angle} ${centerX} ${centerY})`, }; }; diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.html b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.html index 044118269..59566a393 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.html +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.html @@ -10,18 +10,23 @@ > @if (isDiagramInitialized()) { - @for (node of minimapNodes(); track node.id) { - - } + + @for (item of minimapNodes(); track item.bounds.id) { + @if (item.template) { + + + + } @else { + + } + } + @if (hasValidViewport()) { (1); + /** Position of the minimap panel within the diagram container. */ position = input('bottom-right'); @@ -55,10 +70,47 @@ export class NgDiagramMinimapComponent implements OnDestroy { /** Height of the minimap in pixels. */ height = input(150); + /** + * Optional callback function to customize node styling. + * Return style properties to override defaults, or null/undefined to use CSS defaults. + * + * @example + * ```typescript + * nodeStyle = (node: Node) => ({ + * fill: node.type === 'database' ? '#4CAF50' : '#9E9E9E', + * opacity: node.selected ? 1 : 0.6, + * }); + * ``` + */ + nodeStyle = input(); + + /** + * Optional template map for complete control over node rendering per node type. + * Components registered in the map should render SVG elements. + * + * @example + * ```typescript + * const minimapTemplateMap = new NgDiagramMinimapNodeTemplateMap([ + * ['database', DatabaseMinimapNodeComponent], + * ['api', ApiMinimapNodeComponent], + * ]); + * + * // Usage: + * + * ``` + */ + minimapNodeTemplateMap = input(new NgDiagramMinimapNodeTemplateMap()); + constructor() { this.minimapProviderService.register(this); } + /** @ignore */ + ngAfterViewInit(): void { + // Cache stroke padding after view init to avoid layout thrashing + this.strokePadding.set(this.getStrokePaddingFromCss()); + } + /** @ignore */ ngOnDestroy(): void { this.minimapProviderService.unregister(); @@ -68,23 +120,55 @@ export class NgDiagramMinimapComponent implements OnDestroy { nodes = this.renderer.nodes; viewport = this.renderer.viewport; - diagramBounds = computed(() => { - const nodes = this.nodes(); - return this.modelService.computePartsBounds(nodes, []); + hasValidViewport = computed(() => { + const viewport = this.viewport(); + return !!viewport.width && !!viewport.height && viewport.width > 0 && viewport.height > 0; }); - transform = computed(() => - calculateMinimapTransform(this.combinedBounds(), this.width(), this.height(), this.getStrokePadding()) + viewportRect = computed(() => transformViewportToMinimapSpace(this.viewport(), this.transform())); + + /** + * @internal + * Main transform for minimap - updates when viewport or diagram bounds change. + * Used for SVG group transform and viewport rect calculation. + */ + protected transform = computed(() => + calculateMinimapTransform(this.combinedBounds(), this.width(), this.height(), this.strokePadding()) ); - minimapNodes = computed(() => this.nodes().map((node: Node) => transformNodeToMinimapSpace(node, this.transform()))); + /** + * @internal + * SVG transform attribute for the nodes group. + * Converts diagram coordinates to minimap coordinates. + */ + protected nodesGroupTransform = computed(() => { + const t = this.transform(); + return `translate(${t.offsetX}, ${t.offsetY}) scale(${t.scale})`; + }); - hasValidViewport = computed(() => { - const viewport = this.viewport(); - return !!viewport.width && !!viewport.height && viewport.width > 0 && viewport.height > 0; + /** + * @internal + * Pre-computed minimap node data in DIAGRAM coordinates (not minimap coordinates). + * The SVG group transform handles the coordinate conversion, so nodes only recalculate + * when diagram content changes, NOT during pan/zoom. + */ + protected minimapNodes = computed((): MinimapNodeData[] => { + const nodes = this.nodes(); + const styleFn = this.nodeStyle(); + const templateMap = this.minimapNodeTemplateMap(); + + return nodes.map((node) => ({ + bounds: extractNodeBounds(node), + diagramNode: node, + nodeStyle: styleFn?.(node) ?? {}, + template: node.type ? (templateMap.get(node.type) ?? null) : null, + })); }); - viewportRect = computed(() => transformViewportToMinimapSpace(this.viewport(), this.transform())); + private diagramBounds = computed(() => { + const nodes = this.nodes(); + return this.modelService.computePartsBounds(nodes, []); + }); private viewportBoundsInDiagramSpace = computed(() => convertViewportToDiagramBounds(this.viewport())); @@ -96,7 +180,7 @@ export class NgDiagramMinimapComponent implements OnDestroy { * Reads viewport stroke width from CSS to use as internal padding. * This prevents the viewport rectangle stroke from being clipped at minimap edges. */ - private getStrokePadding(): number { + private getStrokePaddingFromCss(): number { const style = getComputedStyle(this.elementRef.nativeElement); const strokeWidth = style.getPropertyValue(this.VIEWPORT_STROKE_WIDTH_CSS_VAR).trim(); diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.types.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.types.ts index b5a6c114f..2f2649b60 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.types.ts +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.types.ts @@ -1,3 +1,6 @@ +import { InputSignal, Type } from '@angular/core'; +import { Node } from '../../../core/src'; + /** * Represents the calculated transform data for minimap rendering. */ @@ -8,15 +11,31 @@ export interface MinimapTransform { } /** - * Represents a node's visual representation in the minimap. + * Pre-computed node data for minimap rendering. + * Contains transformed bounds, original node reference, styling, and optional custom template. + */ +export interface MinimapNodeData { + bounds: MinimapBounds; + diagramNode: Node; + nodeStyle: MinimapNodeStyle; + template: Type | null; +} + +/** + * Bounding box and transform for a node in minimap coordinate space. + * + * @public + * @since 1.0.0 + * @category Types/Minimap */ -export interface MinimapNode { +export interface MinimapBounds { id: string; x: number; y: number; width: number; height: number; angle: number; + transform: string; } /** @@ -28,3 +47,111 @@ export interface MinimapViewportRect { width: number; height: number; } + +/** + * Available shapes for minimap node rendering. + * + * @public + * @since 1.0.0 + * @category Types/Minimap + */ +export type MinimapNodeShape = 'rect' | 'circle' | 'ellipse'; + +/** + * Style properties that can be applied to minimap nodes. + * All properties are optional - unset properties use CSS defaults. + * + * @public + * @since 1.0.0 + * @category Types/Minimap + */ +export interface MinimapNodeStyle { + /** Shape of the node in the minimap. Defaults to 'rect'. */ + shape?: MinimapNodeShape; + /** Fill color for the node */ + fill?: string; + /** Stroke color for the node */ + stroke?: string; + /** Stroke width in pixels */ + strokeWidth?: number; + /** Opacity from 0 to 1 */ + opacity?: number; + /** CSS class to apply to the node */ + cssClass?: string; +} + +/** + * Function signature for the nodeStyle callback. + * Return style properties to override defaults, or null/undefined to use defaults. + * + * @public + * @since 1.0.0 + * @category Types/Minimap + * + * @example + * ```typescript + * const nodeStyle: MinimapNodeStyleFn = (node) => ({ + * fill: node.type === 'database' ? '#4CAF50' : '#9E9E9E', + * opacity: node.selected ? 1 : 0.6, + * }); + * ``` + */ +export type MinimapNodeStyleFn = (node: Node) => MinimapNodeStyle | null | undefined; + +/** + * Interface for custom minimap node components. + * Components implementing this interface can be registered in NgDiagramMinimapNodeTemplateMap + * to customize how specific node types are rendered in the minimap. + * + * Custom templates are rendered inside a foreignObject that handles positioning and sizing, + * so the component only needs to render content that fills its container. + * + * @public + * @since 1.0.0 + * @category Types/Minimap + * + * @example + * ```typescript + * @Component({ + * selector: 'my-minimap-node', + * standalone: true, + * template: ` + *
+ * {{ node().type }} + *
+ * `, + * styles: [`.minimap-icon { width: 100%; height: 100%; }`] + * }) + * export class MyMinimapNodeComponent implements NgDiagramMinimapNodeTemplate { + * node = input.required(); + * nodeStyle = input(); // Required by interface, can be ignored if not needed + * } + * ``` + */ +export interface NgDiagramMinimapNodeTemplate { + /** Input signal containing the original Node object for accessing node data, type, etc. */ + node: InputSignal; + /** Input signal for style overrides computed by nodeStyle callback. Can be ignored if not needed. */ + nodeStyle: InputSignal; +} + +/** + * Map that associates node type names with their corresponding minimap Angular component classes. + * Used by ng-diagram-minimap to determine which custom component to render based on node type. + * + * @public + * @since 1.0.0 + * @category Types/Minimap + * + * @example + * ```typescript + * const minimapTemplateMap = new NgDiagramMinimapNodeTemplateMap([ + * ['database', DatabaseMinimapNodeComponent], + * ['api', ApiMinimapNodeComponent], + * ]); + * + * // Usage in template: + * + * ``` + */ +export class NgDiagramMinimapNodeTemplateMap extends Map> {} diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/types/panel-position.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/types/panel-position.ts index 57c9fd749..695bb26ff 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/types/panel-position.ts +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/types/panel-position.ts @@ -2,7 +2,7 @@ * Position for diagram overlay panels (minimap, watermark, etc.). * * @public - * @since 0.9.1 + * @since 1.0.0 * @category Types */ export type NgDiagramPanelPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; diff --git a/packages/ng-diagram/projects/ng-diagram/src/public-api.ts b/packages/ng-diagram/projects/ng-diagram/src/public-api.ts index 37ef9e081..55ada9db0 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/public-api.ts +++ b/packages/ng-diagram/projects/ng-diagram/src/public-api.ts @@ -57,6 +57,7 @@ export { NgDiagramService } from './lib/public-services/ng-diagram.service'; // Configuration helpers export { configureShortcuts } from './core/src'; +export { NgDiagramMinimapNodeTemplateMap } from './lib/components/minimap/ng-diagram-minimap.types'; export { initializeModel } from './lib/model/initialize-model'; export { provideNgDiagram } from './lib/providers/ng-diagram.providers'; export { NgDiagramEdgeTemplateMap } from './lib/types/edge-template-map'; @@ -64,6 +65,12 @@ export { NgDiagramNodeTemplateMap } from './lib/types/node-template-map'; export { createMiddlewares } from './lib/utils/create-middlewares'; // Types +export type { + MinimapNodeShape, + MinimapNodeStyle, + MinimapNodeStyleFn, + NgDiagramMinimapNodeTemplate, +} from './lib/components/minimap/ng-diagram-minimap.types'; export type { NgDiagramConfig } from './lib/types/config'; export type { NgDiagramEdgeTemplate } from './lib/types/edge-template-map'; export type { PointerInputEvent } from './lib/types/event'; From af914c758fd6c011097790565219547135871de0 Mon Sep 17 00:00:00 2001 From: Piotr Blaszczyk Date: Thu, 29 Jan 2026 22:45:44 +0100 Subject: [PATCH 3/6] Add zoom-controls --- .../minimap/ng-diagram-minimap.component.html | 11 +++-- .../minimap/ng-diagram-minimap.component.scss | 15 +++--- .../minimap/ng-diagram-minimap.component.ts | 33 +++++++------ .../panel/ng-diagram-panel.component.scss | 11 +++++ .../panel/ng-diagram-panel.component.ts | 33 +++++++++++++ .../watermark/watermark.component.ts | 10 ++-- .../ng-diagram-zoom-controls.component.scss | 40 ++++++++++++++++ .../ng-diagram-zoom-controls.component.ts | 48 +++++++++++++++++++ .../src/lib/providers/ng-diagram.providers.ts | 4 +- .../ng-diagram-viewport.service.ts | 34 +++++++++++-- .../minimap-provider.service.ts | 21 -------- .../panel-registry/panel-registry.service.ts | 31 ++++++++++++ .../ng-diagram/src/lib/styles/primitives.css | 2 + .../ng-diagram/src/lib/styles/styles.css | 12 +++++ .../ng-diagram/src/lib/styles/tokens.css | 8 ++++ 15 files changed, 255 insertions(+), 58 deletions(-) create mode 100644 packages/ng-diagram/projects/ng-diagram/src/lib/components/panel/ng-diagram-panel.component.scss create mode 100644 packages/ng-diagram/projects/ng-diagram/src/lib/components/panel/ng-diagram-panel.component.ts create mode 100644 packages/ng-diagram/projects/ng-diagram/src/lib/components/zoom-controls/ng-diagram-zoom-controls.component.scss create mode 100644 packages/ng-diagram/projects/ng-diagram/src/lib/components/zoom-controls/ng-diagram-zoom-controls.component.ts delete mode 100644 packages/ng-diagram/projects/ng-diagram/src/lib/services/minimap-provider/minimap-provider.service.ts create mode 100644 packages/ng-diagram/projects/ng-diagram/src/lib/services/panel-registry/panel-registry.service.ts diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.html b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.html index 59566a393..189b61907 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.html +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.html @@ -1,4 +1,4 @@ -
+ - + } @else { @@ -38,4 +40,7 @@ } } -
+ @if (showZoomControls()) { + + } + diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.scss b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.scss index d1fc28f92..bf1064843 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.scss +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.scss @@ -25,15 +25,6 @@ } } -.minimap-container { - overflow: hidden; - border-radius: var(--ngd-minimap-border-radius); - padding: var(--ngd-minimap-padding); - background-color: var(--ngd-minimap-background); - box-shadow: 0 8px 16px -4px var(--ngd-minimap-shadow-color); - border: 1px solid var(--ngd-minimap-border-color); -} - .minimap-svg { display: block; cursor: pointer; @@ -49,3 +40,9 @@ fill: transparent; pointer-events: none; } + +.minimap-footer { + display: flex; + justify-content: center; + align-items: center; +} diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.ts index 771daf5d3..5fe185f6c 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.ts +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.ts @@ -7,13 +7,13 @@ import { ElementRef, inject, input, - OnDestroy, signal, } from '@angular/core'; import { NgDiagramModelService } from '../../public-services/ng-diagram-model.service'; -import { MinimapProviderService } from '../../services/minimap-provider/minimap-provider.service'; import { RendererService } from '../../services/renderer/renderer.service'; import { NgDiagramPanelPosition } from '../../types/panel-position'; +import { NgDiagramPanelComponent } from '../panel/ng-diagram-panel.component'; +import { NgDiagramZoomControlsComponent } from '../zoom-controls/ng-diagram-zoom-controls.component'; import { NgDiagramDefaultMinimapNodeComponent } from './default-node/ng-diagram-default-minimap-node.component'; import { NgDiagramMinimapNavigationDirective } from './ng-diagram-minimap-navigation.directive'; import { @@ -42,7 +42,19 @@ import { MinimapNodeData, MinimapNodeStyleFn, NgDiagramMinimapNodeTemplateMap } @Component({ selector: 'ng-diagram-minimap', standalone: true, - imports: [NgDiagramMinimapNavigationDirective, CommonModule, NgDiagramDefaultMinimapNodeComponent], + imports: [ + NgDiagramMinimapNavigationDirective, + CommonModule, + NgDiagramDefaultMinimapNodeComponent, + NgDiagramZoomControlsComponent, + NgDiagramPanelComponent, + + NgDiagramMinimapNavigationDirective, + CommonModule, + NgDiagramDefaultMinimapNodeComponent, + NgDiagramZoomControlsComponent, + NgDiagramPanelComponent, + ], templateUrl: './ng-diagram-minimap.component.html', styleUrls: ['./ng-diagram-minimap.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, @@ -50,13 +62,12 @@ import { MinimapNodeData, MinimapNodeStyleFn, NgDiagramMinimapNodeTemplateMap } '[class]': 'position()', }, }) -export class NgDiagramMinimapComponent implements AfterViewInit, OnDestroy { +export class NgDiagramMinimapComponent implements AfterViewInit { private readonly VIEWPORT_STROKE_WIDTH_CSS_VAR = '--ngd-minimap-viewport-stroke-width'; private readonly modelService = inject(NgDiagramModelService); private readonly renderer = inject(RendererService); private readonly elementRef = inject(ElementRef); - private readonly minimapProviderService = inject(MinimapProviderService); /** Cached stroke padding to avoid layout thrashing from repeated getComputedStyle calls. */ private strokePadding = signal(1); @@ -70,6 +81,9 @@ export class NgDiagramMinimapComponent implements AfterViewInit, OnDestroy { /** Height of the minimap in pixels. */ height = input(150); + /** Whether to show zoom controls in the minimap footer. */ + showZoomControls = input(true); + /** * Optional callback function to customize node styling. * Return style properties to override defaults, or null/undefined to use CSS defaults. @@ -101,21 +115,12 @@ export class NgDiagramMinimapComponent implements AfterViewInit, OnDestroy { */ minimapNodeTemplateMap = input(new NgDiagramMinimapNodeTemplateMap()); - constructor() { - this.minimapProviderService.register(this); - } - /** @ignore */ ngAfterViewInit(): void { // Cache stroke padding after view init to avoid layout thrashing this.strokePadding.set(this.getStrokePaddingFromCss()); } - /** @ignore */ - ngOnDestroy(): void { - this.minimapProviderService.unregister(); - } - isDiagramInitialized = this.renderer.isInitialized; nodes = this.renderer.nodes; viewport = this.renderer.viewport; diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/panel/ng-diagram-panel.component.scss b/packages/ng-diagram/projects/ng-diagram/src/lib/components/panel/ng-diagram-panel.component.scss new file mode 100644 index 000000000..aac47e382 --- /dev/null +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/panel/ng-diagram-panel.component.scss @@ -0,0 +1,11 @@ +:host { + overflow: hidden; + border-radius: var(--ngd-minimap-border-radius); + padding: var(--ngd-minimap-padding); + background-color: var(--ngd-minimap-background); + box-shadow: 0 8px 16px -4px var(--ngd-minimap-shadow-color); + border: 1px solid var(--ngd-minimap-border-color); + display: flex; + flex-direction: column; + gap: var(--ngd-minimap-padding); +} diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/panel/ng-diagram-panel.component.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/panel/ng-diagram-panel.component.ts new file mode 100644 index 000000000..9b6bcb728 --- /dev/null +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/panel/ng-diagram-panel.component.ts @@ -0,0 +1,33 @@ +import { AfterViewInit, ChangeDetectionStrategy, Component, inject, input, OnDestroy } from '@angular/core'; +import { PanelRegistryService } from '../../services/panel-registry/panel-registry.service'; +import { NgDiagramPanelPosition } from '../../types/panel-position'; + +/** + * A generic panel container component that registers itself with the panel registry. + * Used internally to wrap overlay content and coordinate positioning with other components. + * @internal + */ +@Component({ + selector: 'ng-diagram-panel', + standalone: true, + template: '', + styleUrls: ['./ng-diagram-panel.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class]': 'position()', + }, +}) +export class NgDiagramPanelComponent implements AfterViewInit, OnDestroy { + private readonly panelRegistry = inject(PanelRegistryService); + + /** Position of the panel within the diagram container. */ + position = input('bottom-right'); + + ngAfterViewInit(): void { + this.panelRegistry?.register(this); + } + + ngOnDestroy(): void { + this.panelRegistry?.unregister(); + } +} diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/watermark/watermark.component.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/watermark/watermark.component.ts index 1b353e082..c73fa4b7b 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/watermark/watermark.component.ts +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/watermark/watermark.component.ts @@ -1,5 +1,5 @@ import { Component, computed, inject } from '@angular/core'; -import { MinimapProviderService } from '../../services/minimap-provider/minimap-provider.service'; +import { PanelRegistryService } from '../../services/panel-registry/panel-registry.service'; import { NgDiagramPanelPosition } from '../../types/panel-position'; @Component({ @@ -15,14 +15,14 @@ export class NgDiagramWatermarkComponent { private readonly DEFAULT_POSITION: NgDiagramPanelPosition = 'bottom-right'; private readonly ALTERNATIVE_POSITION: NgDiagramPanelPosition = 'top-right'; - private readonly minimapProviderService = inject(MinimapProviderService); + private readonly panelRegistry = inject(PanelRegistryService); /** - * Computes the watermark position based on minimap position. - * If minimap is at the same position, watermark moves to avoid overlap. + * Computes the watermark position based on panel position. + * If panel is at the same position, watermark moves to avoid overlap. */ position = computed(() => { - if (this.minimapProviderService.position() === this.DEFAULT_POSITION) { + if (this.panelRegistry.position() === this.DEFAULT_POSITION) { return this.ALTERNATIVE_POSITION; } diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/zoom-controls/ng-diagram-zoom-controls.component.scss b/packages/ng-diagram/projects/ng-diagram/src/lib/components/zoom-controls/ng-diagram-zoom-controls.component.scss new file mode 100644 index 000000000..9d895de14 --- /dev/null +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/zoom-controls/ng-diagram-zoom-controls.component.scss @@ -0,0 +1,40 @@ +.zoom-controls-container { + display: flex; + align-items: center; + justify-content: space-between; + + .label { + text-align: center; + font-size: var(--ngd-zoom-controls-font-size); + font-weight: var(--ngd-zoom-controls-font-weight); + color: var(--ngd-zoom-controls-color); + user-select: none; + } +} + +.nav-button { + width: var(--ngd-nav-button-size); + height: var(--ngd-nav-button-size); + color: var(--ngd-nav-button-color); + border-radius: var(--ngd-nav-button-border-radius); + padding: var(--ngd-nav-button-padding); + border: none; + background: none; + box-sizing: content-box; + cursor: pointer; + font-size: var(--ngd-nav-button-size); + line-height: var(--ngd-nav-button-size); + + &:hover:not(:disabled) { + background-color: var(--ngd-nav-button-background-color-hover); + } + + &:active { + color: var(--ngd-nav-button-color-active); + } + + &:disabled { + color: var(--ngd-nav-button-color-disabled); + cursor: default; + } +} diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/zoom-controls/ng-diagram-zoom-controls.component.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/zoom-controls/ng-diagram-zoom-controls.component.ts new file mode 100644 index 000000000..929a3a079 --- /dev/null +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/zoom-controls/ng-diagram-zoom-controls.component.ts @@ -0,0 +1,48 @@ +import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core'; +import { NgDiagramViewportService } from '../../public-services/ng-diagram-viewport.service'; + +/** + * A zoom controls component that displays zoom in/out buttons and current zoom level. + */ +@Component({ + selector: 'ng-diagram-zoom-controls', + standalone: true, + template: ` +
+ + {{ zoomPercentage() }}% + +
+ `, + styleUrls: ['./ng-diagram-zoom-controls.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NgDiagramZoomControlsComponent { + private readonly viewportService = inject(NgDiagramViewportService); + + step = input(0.1); + + zoomPercentage = computed(() => Math.round(this.viewportService.scale() * 100)); + + canZoomIn = computed(() => { + const newScale = this.viewportService.scale() + this.step(); + return newScale <= this.viewportService.maxZoom; + }); + + canZoomOut = computed(() => { + const newScale = this.viewportService.scale() - this.step(); + return newScale >= this.viewportService.minZoom; + }); + + zoomIn(): void { + const currentScale = this.viewportService.scale(); + const factor = (currentScale + this.step()) / currentScale; + this.viewportService.zoom(factor); + } + + zoomOut(): void { + const currentScale = this.viewportService.scale(); + const factor = (currentScale - this.step()) / currentScale; + this.viewportService.zoom(factor); + } +} diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/providers/ng-diagram.providers.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/providers/ng-diagram.providers.ts index 6d8e0cbfd..e7120247b 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/providers/ng-diagram.providers.ts +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/providers/ng-diagram.providers.ts @@ -14,7 +14,7 @@ import { InputEventsRouterService } from '../services/input-events/input-events- import { LinkingEventService } from '../services/input-events/linking-event.service'; import { ManualLinkingService } from '../services/input-events/manual-linking.service'; import { MarkerRegistryService } from '../services/marker-registry/marker-registry.service'; -import { MinimapProviderService } from '../services/minimap-provider/minimap-provider.service'; +import { PanelRegistryService } from '../services/panel-registry/panel-registry.service'; import { PaletteService } from '../services/palette/palette.service'; import { RendererService } from '../services/renderer/renderer.service'; import { TemplateProviderService } from '../services/template-provider/template-provider.service'; @@ -70,6 +70,6 @@ export function provideNgDiagram(): Provider[] { ManualLinkingService, TemplateProviderService, MarkerRegistryService, - MinimapProviderService, + PanelRegistryService, ]; } diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/public-services/ng-diagram-viewport.service.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/public-services/ng-diagram-viewport.service.ts index d49ddb559..859f24be9 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/public-services/ng-diagram-viewport.service.ts +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/public-services/ng-diagram-viewport.service.ts @@ -38,6 +38,30 @@ export class NgDiagramViewportService extends NgDiagramBaseService { */ scale = computed(() => this.renderer.viewport().scale || 1); + /** + * Returns the minimum zoom scale from the diagram configuration. + */ + get minZoom(): number { + return this.flowCore.config.zoom.min; + } + + /** + * Returns the maximum zoom scale from the diagram configuration. + */ + get maxZoom(): number { + return this.flowCore.config.zoom.max; + } + + /** + * Returns true if the current zoom level is below the maximum and can be increased. + */ + canZoomIn = computed(() => this.scale() < this.maxZoom); + + /** + * Returns true if the current zoom level is above the minimum and can be decreased. + */ + canZoomOut = computed(() => this.scale() > this.minZoom); + // =================== // POSITION CONVERSION METHODS // =================== @@ -97,13 +121,15 @@ export class NgDiagramViewportService extends NgDiagramBaseService { /** * Zooms the viewport by the specified factor. - * @param factor The factor to zoom by. + * @param factor The factor to zoom by (e.g., 1.1 for 10% zoom in, 0.9 for 10% zoom out). * @param center The center point to zoom towards. */ zoom(factor: number, center?: Point | undefined) { - const x = center?.x || this.viewport().x; - const y = center?.y || this.viewport().y; - this.flowCore.commandHandler.emit('zoom', { scale: factor, x, y }); + const currentScale = this.scale(); + const newScale = currentScale * factor; + const x = center?.x ?? this.viewport().x; + const y = center?.y ?? this.viewport().y; + this.flowCore.commandHandler.emit('zoom', { scale: newScale, x, y }); } /** diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/services/minimap-provider/minimap-provider.service.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/services/minimap-provider/minimap-provider.service.ts deleted file mode 100644 index bd908d5b4..000000000 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/services/minimap-provider/minimap-provider.service.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { computed, Injectable, signal } from '@angular/core'; -import type { NgDiagramMinimapComponent } from '../../components/minimap/ng-diagram-minimap.component'; - -/** - * Service that provides access to the registered minimap component. - * Used by other components (like watermark) to coordinate their positioning. - */ -@Injectable() -export class MinimapProviderService { - private readonly minimap = signal(null); - - position = computed(() => this.minimap()?.position()); - - register(component: NgDiagramMinimapComponent): void { - this.minimap.set(component); - } - - unregister(): void { - this.minimap.set(null); - } -} diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/services/panel-registry/panel-registry.service.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/services/panel-registry/panel-registry.service.ts new file mode 100644 index 000000000..7aea4e683 --- /dev/null +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/services/panel-registry/panel-registry.service.ts @@ -0,0 +1,31 @@ +import { computed, Injectable, Signal, signal } from '@angular/core'; +import { NgDiagramPanelPosition } from '../../types/panel-position'; + +/** + * Interface for panel components that can register with the panel registry. + * @internal + */ +export interface RegisterablePanel { + position: Signal; +} + +/** + * Service that tracks registered panel components. + * Used by other components (like watermark) to coordinate their positioning + * and avoid overlapping with panels. + * @internal + */ +@Injectable() +export class PanelRegistryService { + private readonly panel = signal(null); + + position = computed(() => this.panel()?.position()); + + register(component: RegisterablePanel): void { + this.panel.set(component); + } + + unregister(): void { + this.panel.set(null); + } +} diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/styles/primitives.css b/packages/ng-diagram/projects/ng-diagram/src/lib/styles/primitives.css index fad6909e3..8e29ed09c 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/styles/primitives.css +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/styles/primitives.css @@ -13,11 +13,13 @@ --ngd-colors-gray-650: #383a40; --ngd-colors-gray-700: #27282b; --ngd-colors-gray-800: #151516; + --ngd-colors-gray-900-5: rgba(7, 7, 8, 0.05); --ngd-colors-gray-900-20: rgba(7, 7, 8, 0.2); --ngd-colors-gray-900-50: rgba(7, 7, 8, 0.5); --ngd-colors-acc1-400: #a977ff; --ngd-colors-acc1-500: #9140ff; --ngd-colors-acc1-500-40: rgba(145, 64, 255, 0.4); --ngd-colors-acc1-500-50: rgba(145, 64, 255, 0.5); + --ngd-colors-acc1-600: #891aff; --ngd-colors-acc4-500: #1096e7; } diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/styles/styles.css b/packages/ng-diagram/projects/ng-diagram/src/lib/styles/styles.css index 2f4e52915..83a872131 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/styles/styles.css +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/styles/styles.css @@ -77,6 +77,18 @@ --ngd-minimap-viewport-stroke-color: var(--ngd-colors-gray-400); --ngd-minimap-viewport-stroke-width: 1; + + --ngd-nav-button-color: var(--ngd-nav-button-primary-default); + --ngd-nav-button-size: 1.25rem; + --ngd-nav-button-border-radius: 0.5rem; + --ngd-nav-button-padding: 0.6875rem; + --ngd-nav-button-background-color-hover: var(--ngd-nav-button-primary-hover); + --ngd-nav-button-color-active: var(--ngd-nav-button-primary-pressed); + --ngd-nav-button-color-disabled: var(--ngd-nav-button-primary-disabled); + + --ngd-zoom-controls-font-size: 0.8125rem; + --ngd-zoom-controls-font-weight: 500; + --ngd-zoom-controls-color: var(--ngd-nav-button-primary-default); } .ng-diagram-port-hoverable .ng-diagram-port:hover, diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/styles/tokens.css b/packages/ng-diagram/projects/ng-diagram/src/lib/styles/tokens.css index da4125d8b..e46718977 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/styles/tokens.css +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/styles/tokens.css @@ -4,6 +4,10 @@ :root { --ngd-shadow: var(--ngd-colors-gray-900-20); --ngd-txt-primary-default: var(--ngd-colors-gray-800); + --ngd-nav-button-primary-default: var(--ngd-colors-gray-600); + --ngd-nav-button-primary-pressed: var(--ngd-colors-acc1-600); + --ngd-nav-button-primary-hover: var(--ngd-colors-gray-900-5); + --ngd-nav-button-primary-disabled: var(--ngd-colors-gray-450); --ngd-diagram-background-color: var(--ngd-colors-gray-200); --ngd-ui-bg-primary-default: var(--ngd-colors-gray-100); --ngd-ui-bg-tertiary-default: var(--ngd-colors-gray-200); @@ -33,6 +37,10 @@ html[data-theme='dark'] { --ngd-shadow: var(--ngd-colors-gray-900-50); --ngd-txt-primary-default: var(--ngd-colors-gray-100); + --ngd-nav-button-primary-default: var(--ngd-colors-gray-400); + --ngd-nav-button-primary-pressed: var(--ngd-colors-acc1-500); + --ngd-nav-button-primary-hover: var(--ngd-colors-gray-900-5); + --ngd-nav-button-primary-disabled: var(--ngd-colors-gray-600); --ngd-diagram-background-color: var(--ngd-colors-gray-800); --ngd-ui-bg-primary-default: var(--ngd-colors-gray-700); --ngd-ui-bg-tertiary-default: var(--ngd-colors-gray-800); From 35ce5e2c0668c047e79f367041c17204f48bdce3 Mon Sep 17 00:00:00 2001 From: Piotr Blaszczyk Date: Thu, 29 Jan 2026 22:49:38 +0100 Subject: [PATCH 4/6] api report update --- .../ng-diagram/api-report/ng-diagram.api.md | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/packages/ng-diagram/api-report/ng-diagram.api.md b/packages/ng-diagram/api-report/ng-diagram.api.md index 23d016b78..af26e810e 100644 --- a/packages/ng-diagram/api-report/ng-diagram.api.md +++ b/packages/ng-diagram/api-report/ng-diagram.api.md @@ -506,6 +506,22 @@ export interface MiddlewareHistoryUpdate { stateUpdate: FlowStateUpdate; } +// @public +export type MinimapNodeShape = 'rect' | 'circle' | 'ellipse'; + +// @public +export interface MinimapNodeStyle { + cssClass?: string; + fill?: string; + opacity?: number; + shape?: MinimapNodeShape; + stroke?: string; + strokeWidth?: number; +} + +// @public +export type MinimapNodeStyleFn = (node: Node_2) => MinimapNodeStyle | null | undefined; + // @public (undocumented) export class MobileBoxSelectionDirective { // (undocumented) @@ -821,6 +837,64 @@ export const NgDiagramMath: { calculateEdgePanningForce: (containerBox: Rect, clientPosition: Point, detectionThreshold: number, forceMultiplier: number) => Point | null; }; +// @public +export class NgDiagramMinimapComponent implements AfterViewInit { + // (undocumented) + hasValidViewport: Signal; + height: InputSignal; + // (undocumented) + isDiagramInitialized: WritableSignal; + // @internal + protected minimapNodes: Signal; + minimapNodeTemplateMap: InputSignal; + // (undocumented) + ngAfterViewInit(): void; + // (undocumented) + nodes: WritableSignal; + // @internal + protected nodesGroupTransform: Signal; + nodeStyle: InputSignal; + position: InputSignal; + showZoomControls: InputSignal; + // @internal + protected transform: Signal; + // (undocumented) + viewport: WritableSignal; + // (undocumented) + viewportRect: Signal; + width: InputSignal; + // (undocumented) + static ɵcmp: i0.ɵɵComponentDeclaration; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; +} + +// @public +export class NgDiagramMinimapNavigationDirective implements OnDestroy { + // (undocumented) + ngOnDestroy(): void; + // (undocumented) + onPointerDown(event: PointerEvent): void; + // (undocumented) + transform: InputSignal; + // (undocumented) + viewport: InputSignal; + // (undocumented) + static ɵdir: i0.ɵɵDirectiveDeclaration; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; +} + +// @public +export interface NgDiagramMinimapNodeTemplate { + node: InputSignal; + nodeStyle: InputSignal; +} + +// @public +export class NgDiagramMinimapNodeTemplateMap extends Map> { +} + // @public export class NgDiagramModelService extends NgDiagramBaseService implements OnDestroy { constructor(); @@ -983,6 +1057,9 @@ export class NgDiagramPaletteItemPreviewComponent { static ɵfac: i0.ɵɵFactoryDeclaration; } +// @public +export type NgDiagramPanelPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; + // @public export class NgDiagramPortComponent extends NodeContextGuardBase implements OnInit, OnDestroy, AfterContentInit { constructor(); @@ -1074,11 +1151,15 @@ export class NgDiagramServicesAvailabilityCheckerDirective { // @public export class NgDiagramViewportService extends NgDiagramBaseService { + canZoomIn: Signal; + canZoomOut: Signal; centerOnNode(nodeOrId: string | Node_2): void; centerOnRect(rect: Rect): void; clientToFlowPosition(clientPosition: Point): Point; clientToFlowViewportPosition(clientPosition: Point): Point; flowToClientPosition(flowPosition: Point): Point; + get maxZoom(): number; + get minZoom(): number; moveViewport(x: number, y: number): void; moveViewportBy(dx: number, dy: number): void; scale: Signal; From 9b5370e27ab39df5c2ac1074ee6bda4804b12c7f Mon Sep 17 00:00:00 2001 From: Piotr Blaszczyk Date: Fri, 30 Jan 2026 11:36:30 +0100 Subject: [PATCH 5/6] Fix unit test --- .../src/lib/components/diagram/ng-diagram.component.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/diagram/ng-diagram.component.spec.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/diagram/ng-diagram.component.spec.ts index 92afd6cab..b21170b38 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/diagram/ng-diagram.component.spec.ts +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/diagram/ng-diagram.component.spec.ts @@ -13,6 +13,7 @@ import { import { CursorPositionTrackerService } from '../../services/cursor-position-tracker/cursor-position-tracker.service'; import { InputEventsRouterService } from '../../services/input-events/input-events-router.service'; import { MarkerRegistryService } from '../../services/marker-registry/marker-registry.service'; +import { PanelRegistryService } from '../../services/panel-registry/panel-registry.service'; import { TemplateProviderService } from '../../services/template-provider/template-provider.service'; import { NgDiagramComponent } from './ng-diagram.component'; @@ -112,6 +113,7 @@ describe('AngularAdapterDiagramComponent', () => { CursorPositionTrackerService, InputEventsRouterService, MarkerRegistryService, + PanelRegistryService, ], }).compileComponents(); From d650c7d726481316eae39ada43a9ca7a63acdb9d Mon Sep 17 00:00:00 2001 From: Piotr Blaszczyk Date: Mon, 2 Feb 2026 14:18:41 +0100 Subject: [PATCH 6/6] Support touch events and add documentation --- .../angular/minimap/diagram.component.html | 6 + .../angular/minimap/diagram.component.scss | 12 + .../angular/minimap/diagram.component.ts | 60 +++++ .../components/angular/minimap/minimap.astro | 5 + .../Components/NgDiagramMinimapComponent.md | 9 +- .../NgDiagramMinimapNavigationDirective.md | 3 + .../api/Services/NgDiagramViewportService.md | 46 +++- apps/docs/src/content/docs/guides/minimap.mdx | 241 ++++++++++++++++++ apps/docs/src/content/docs/intro/styling.mdx | 11 + ...ng-diagram-minimap-navigation.directive.ts | 97 +++++-- .../minimap/ng-diagram-minimap.component.scss | 1 + 11 files changed, 465 insertions(+), 26 deletions(-) create mode 100644 apps/docs/src/components/angular/minimap/diagram.component.html create mode 100644 apps/docs/src/components/angular/minimap/diagram.component.scss create mode 100644 apps/docs/src/components/angular/minimap/diagram.component.ts create mode 100644 apps/docs/src/components/angular/minimap/minimap.astro create mode 100644 apps/docs/src/content/docs/guides/minimap.mdx diff --git a/apps/docs/src/components/angular/minimap/diagram.component.html b/apps/docs/src/components/angular/minimap/diagram.component.html new file mode 100644 index 000000000..d97445a59 --- /dev/null +++ b/apps/docs/src/components/angular/minimap/diagram.component.html @@ -0,0 +1,6 @@ +
+ + + + +
diff --git a/apps/docs/src/components/angular/minimap/diagram.component.scss b/apps/docs/src/components/angular/minimap/diagram.component.scss new file mode 100644 index 000000000..08c961298 --- /dev/null +++ b/apps/docs/src/components/angular/minimap/diagram.component.scss @@ -0,0 +1,12 @@ +:host { + position: relative; + display: flex; +} + +.diagram { + position: relative; + display: flex; + width: 100%; + height: var(--ng-diagram-height); + border: var(--ng-diagram-border); +} diff --git a/apps/docs/src/components/angular/minimap/diagram.component.ts b/apps/docs/src/components/angular/minimap/diagram.component.ts new file mode 100644 index 000000000..5003d761f --- /dev/null +++ b/apps/docs/src/components/angular/minimap/diagram.component.ts @@ -0,0 +1,60 @@ +import '@angular/compiler'; + +import { Component } from '@angular/core'; +import { + initializeModel, + NgDiagramBackgroundComponent, + NgDiagramComponent, + NgDiagramMinimapComponent, + provideNgDiagram, + type NgDiagramConfig, +} from 'ng-diagram'; + +@Component({ + imports: [ + NgDiagramComponent, + NgDiagramBackgroundComponent, + NgDiagramMinimapComponent, + ], + providers: [provideNgDiagram()], + templateUrl: './diagram.component.html', + styleUrls: ['./diagram.component.scss'], +}) +export class DiagramComponent { + config = { + zoom: { + zoomToFit: { + onInit: true, + padding: [50, 250, 50, 50], + }, + }, + } satisfies NgDiagramConfig; + + model = initializeModel({ + nodes: [ + { id: '1', position: { x: 0, y: 0 }, data: { label: 'Node 1' } }, + { id: '2', position: { x: 300, y: 0 }, data: { label: 'Node 2' } }, + { id: '3', position: { x: 150, y: 150 }, data: { label: 'Node 3' } }, + { id: '4', position: { x: 0, y: 300 }, data: { label: 'Node 4' } }, + { id: '5', position: { x: 300, y: 300 }, data: { label: 'Node 5' } }, + ], + edges: [ + { + id: 'e1', + source: '1', + sourcePort: 'port-right', + target: '2', + targetPort: 'port-left', + data: {}, + }, + { + id: 'e2', + source: '4', + sourcePort: 'port-right', + target: '5', + targetPort: 'port-left', + data: {}, + }, + ], + }); +} diff --git a/apps/docs/src/components/angular/minimap/minimap.astro b/apps/docs/src/components/angular/minimap/minimap.astro new file mode 100644 index 000000000..3df7e17e2 --- /dev/null +++ b/apps/docs/src/components/angular/minimap/minimap.astro @@ -0,0 +1,5 @@ +--- +import { DiagramComponent } from './diagram.component'; +--- + + diff --git a/apps/docs/src/content/docs/api/Components/NgDiagramMinimapComponent.md b/apps/docs/src/content/docs/api/Components/NgDiagramMinimapComponent.md index e31cc0123..c60e455e2 100644 --- a/apps/docs/src/content/docs/api/Components/NgDiagramMinimapComponent.md +++ b/apps/docs/src/content/docs/api/Components/NgDiagramMinimapComponent.md @@ -18,7 +18,6 @@ the diagram viewport to different areas. ## Implements - `AfterViewInit` -- `OnDestroy` ## Properties @@ -77,6 +76,14 @@ Position of the minimap panel within the diagram container. *** +### showZoomControls + +> **showZoomControls**: `InputSignal`\<`boolean`\> + +Whether to show zoom controls in the minimap footer. + +*** + ### width > **width**: `InputSignal`\<`number`\> diff --git a/apps/docs/src/content/docs/api/Directives/NgDiagramMinimapNavigationDirective.md b/apps/docs/src/content/docs/api/Directives/NgDiagramMinimapNavigationDirective.md index a67ec05ad..fea23affb 100644 --- a/apps/docs/src/content/docs/api/Directives/NgDiagramMinimapNavigationDirective.md +++ b/apps/docs/src/content/docs/api/Directives/NgDiagramMinimapNavigationDirective.md @@ -9,6 +9,9 @@ title: "NgDiagramMinimapNavigationDirective" Directive that enables drag navigation on the minimap. Users can drag on the minimap to move the diagram viewport. +Supports both mouse and touch input. +Uses pointer capture for reliable touch tracking on mobile devices. + ## Implements - `OnDestroy` diff --git a/apps/docs/src/content/docs/api/Services/NgDiagramViewportService.md b/apps/docs/src/content/docs/api/Services/NgDiagramViewportService.md index a6d64b765..e0e3f0a5c 100644 --- a/apps/docs/src/content/docs/api/Services/NgDiagramViewportService.md +++ b/apps/docs/src/content/docs/api/Services/NgDiagramViewportService.md @@ -25,6 +25,22 @@ this.viewportService.zoom(1.2); ## Properties +### canZoomIn + +> **canZoomIn**: `Signal`\<`boolean`\> + +Returns true if the current zoom level is below the maximum and can be increased. + +*** + +### canZoomOut + +> **canZoomOut**: `Signal`\<`boolean`\> + +Returns true if the current zoom level is above the minimum and can be decreased. + +*** + ### scale > **scale**: `Signal`\<`number`\> @@ -39,6 +55,34 @@ Returns a computed signal for the scale that safely handles uninitialized state. Returns a computed signal for the viewport that safely handles uninitialized state. +## Accessors + +### maxZoom + +#### Get Signature + +> **get** **maxZoom**(): `number` + +Returns the maximum zoom scale from the diagram configuration. + +##### Returns + +`number` + +*** + +### minZoom + +#### Get Signature + +> **get** **minZoom**(): `number` + +Returns the minimum zoom scale from the diagram configuration. + +##### Returns + +`number` + ## Methods ### centerOnNode() @@ -222,7 +266,7 @@ Zooms the viewport by the specified factor. `number` -The factor to zoom by. +The factor to zoom by (e.g., 1.1 for 10% zoom in, 0.9 for 10% zoom out). ##### center? diff --git a/apps/docs/src/content/docs/guides/minimap.mdx b/apps/docs/src/content/docs/guides/minimap.mdx new file mode 100644 index 000000000..a441ce284 --- /dev/null +++ b/apps/docs/src/content/docs/guides/minimap.mdx @@ -0,0 +1,241 @@ +--- +version: 'since v1.0.0' +title: Minimap +description: How to use and customize the minimap component in ngDiagram +sidebar: + label: Minimap + badge: New +--- + +import CodeViewer from '@components/code-viewer/code-viewer.astro'; +import Minimap from '../../../components/angular/minimap/minimap.astro'; + +The [``](/docs/api/components/ngdiagramminimapcomponent/) component provides a bird's-eye view of your diagram, showing all nodes and the current viewport position. +It supports click-and-drag navigation to quickly pan to different areas of your diagram. + +:::note +The minimap displays only nodes. Edges are not rendered in the minimap view. +::: + +## Basic Usage + +Add the [``](/docs/api/components/ngdiagramminimapcomponent/) component inside your diagram container: + +```html +
+ + + + +
+``` + +:::note[Service Access] +The minimap can be placed anywhere in your template, but it requires access to ngDiagram services. +Make sure the component is within a context where [`provideNgDiagram()`](/docs/api/utilities/providengdiagram/) has been provided. +::: + +:::note[Container Positioning] +The minimap uses `position: absolute` for corner placement via the `position` input. +Ensure the parent container has `position: relative` for proper positioning: + +```css +.diagram-container { + position: relative; +} +``` + +::: + +## Configuration + +### Position and Size + +The minimap can be positioned in any corner using the [`position`](/docs/api/components/ngdiagramminimapcomponent/#position) input and sized with [`width`](/docs/api/components/ngdiagramminimapcomponent/#width) and [`height`](/docs/api/components/ngdiagramminimapcomponent/#height): + +```html + +``` + +See [`NgDiagramPanelPosition`](/docs/api/types/ngdiagrampanelposition/) for available position values. + +### Zoom Controls + +By default, zoom controls are displayed below the minimap. To hide them: + +```html + +``` + +## Customization via CSS Variables + +:::tip[Theming] +Some CSS variables reference design tokens to support light/dark themes. +For proper multi-theme customization, see the [Styling guide](/docs/intro/styling/) to understand how tokens and primitives work. +::: + +### General Appearance + +You can customize the minimap container appearance using CSS variables: + +```css +:root { + /* Container styling */ + --ngd-minimap-background: ...; + --ngd-minimap-border-color: ...; + --ngd-minimap-border-radius: 1rem; + --ngd-minimap-padding: 0.5rem; + --ngd-minimap-margin: 1rem; + --ngd-minimap-shadow-color: ...; + + /* Viewport indicator */ + --ngd-minimap-viewport-stroke-color: ...; + --ngd-minimap-viewport-stroke-width: 1; +} +``` + +### Node Styling + +Default node appearance in the minimap can be controlled via CSS variables: + +```css +:root { + --ngd-minimap-node-color: ...; + --ngd-minimap-node-opacity: 0.8; +} +``` + +### Zoom Controls + +The zoom controls appearance can be customized with these CSS variables: + +```css +:root { + /* Zoom controls text */ + --ngd-zoom-controls-font-size: 0.8125rem; + --ngd-zoom-controls-font-weight: 500; + --ngd-zoom-controls-color: ...; + + /* Navigation buttons (zoom in/out) */ + --ngd-nav-button-color: ...; + --ngd-nav-button-size: 1.25rem; + --ngd-nav-button-border-radius: 0.5rem; + --ngd-nav-button-padding: 0.6875rem; + --ngd-nav-button-background-color-hover: ...; + --ngd-nav-button-color-active: ...; + --ngd-nav-button-color-disabled: ...; +} +``` + +## Customization via Style Function + +For dynamic node styling based on [`Node`](/docs/api/types/model/node/) properties, use the [`nodeStyle`](/docs/api/components/ngdiagramminimapcomponent/#nodestyle) input. +This accepts a [`MinimapNodeStyleFn`](/docs/api/types/minimap/minimapnodestylefn/) callback: + +```typescript +import { MinimapNodeStyle, Node } from 'ng-diagram'; + +nodeStyle = (node: Node): MinimapNodeStyle => { + const style: MinimapNodeStyle = {}; + + // Different shape for specific nodes + if (node.type === 'database') { + style.shape = 'circle'; + } + + // Highlight selected nodes + if (node.selected) { + style.stroke = '#2196F3'; + style.strokeWidth = 2; + } + + // Custom fill based on node data + if (node.data?.status === 'error') { + style.fill = '#f44336'; + } + + return style; +}; +``` + +```html + +``` + +See [`MinimapNodeStyle`](/docs/api/types/minimap/minimapnodestyle/) for all available style properties. + +## Customization via Templates + +For complete control over node rendering, you can provide custom Angular components per node type using [`minimapNodeTemplateMap`](/docs/api/components/ngdiagramminimapcomponent/#minimapnodetemplatemap). +This is useful when you need to display icons, images, or complex visuals in the minimap. + +:::caution[Performance Consideration] +Custom templates use `foreignObject` and Angular components, which can impact performance with large diagrams. +Use templates sparingly and only for node types that truly require custom rendering. +For simple styling changes, prefer the `nodeStyle` function or CSS variables. +::: + +### Creating a Custom Template + +Your component must implement the [`NgDiagramMinimapNodeTemplate`](/docs/api/types/minimap/ngdiagramminimapnodetemplate/) interface: + +```typescript +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { MinimapNodeStyle, NgDiagramMinimapNodeTemplate, Node } from 'ng-diagram'; + +@Component({ + selector: 'app-image-minimap-node', + standalone: true, + template: ``, + styles: [ + ` + :host { + display: contents; + + img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + } + } + `, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ImageMinimapNodeComponent implements NgDiagramMinimapNodeTemplate { + node = input.required(); + nodeStyle = input(); + + get imageUrl(): string { + const data = this.node().data as { imageUrl?: string }; + return data?.imageUrl ?? 'placeholder.png'; + } +} +``` + +### Registering Templates + +Create a [`NgDiagramMinimapNodeTemplateMap`](/docs/api/types/minimap/ngdiagramminimapnodetemplatemap/) and pass it to the minimap: + +```typescript +import { NgDiagramMinimapNodeTemplateMap } from 'ng-diagram'; + +minimapNodeTemplateMap = new NgDiagramMinimapNodeTemplateMap([ + ['image', ImageMinimapNodeComponent], + ['custom-type', CustomMinimapNodeComponent], +]); +``` + +```html + +``` + +Nodes with types not in the map will use the default rectangle rendering. + +## Example + + + + +--- diff --git a/apps/docs/src/content/docs/intro/styling.mdx b/apps/docs/src/content/docs/intro/styling.mdx index 98187e403..0644da364 100644 --- a/apps/docs/src/content/docs/intro/styling.mdx +++ b/apps/docs/src/content/docs/intro/styling.mdx @@ -31,18 +31,29 @@ Base color values and fundamental design tokens defined in `primitives`. These a ```css :root { + /* Gray scale */ --ngd-colors-gray-100: /* white */; + --ngd-colors-gray-200: /* near white */; --ngd-colors-gray-300: /* light gray */; --ngd-colors-gray-400: /* medium light gray */; + --ngd-colors-gray-450: /* medium gray light */; --ngd-colors-gray-500: /* medium gray */; --ngd-colors-gray-600: /* medium dark gray */; --ngd-colors-gray-650: /* dark gray */; --ngd-colors-gray-700: /* darker gray */; --ngd-colors-gray-800: /* darkest gray */; + --ngd-colors-gray-900-5: /* near black with 5% opacity */; + --ngd-colors-gray-900-20: /* near black with 20% opacity */; + --ngd-colors-gray-900-50: /* near black with 50% opacity */; + + /* Primary accent */ --ngd-colors-acc1-400: /* primary accent light */; --ngd-colors-acc1-500: /* primary accent */; --ngd-colors-acc1-500-40: /* primary accent with 40% opacity */; --ngd-colors-acc1-500-50: /* primary accent with 50% opacity */; + --ngd-colors-acc1-600: /* primary accent dark */; + + /* Secondary accent */ --ngd-colors-acc4-500: /* secondary accent */; } ``` diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap-navigation.directive.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap-navigation.directive.ts index 9758002a7..1af55786a 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap-navigation.directive.ts +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap-navigation.directive.ts @@ -3,10 +3,24 @@ import { Viewport } from '../../../core/src'; import { NgDiagramViewportService } from '../../public-services/ng-diagram-viewport.service'; import { MinimapTransform } from './ng-diagram-minimap.types'; +interface Point { + x: number; + y: number; +} + +interface DragState { + isDragging: boolean; + lastPosition: Point; + pointerId: number | null; +} + /** * Directive that enables drag navigation on the minimap. * Users can drag on the minimap to move the diagram viewport. * + * Supports both mouse and touch input. + * Uses pointer capture for reliable touch tracking on mobile devices. + * * @public * @since 1.0.0 * @category Directives @@ -24,9 +38,11 @@ export class NgDiagramMinimapNavigationDirective implements OnDestroy { transform = input.required(); viewport = input.required(); - private isDragging = false; - private lastClientX = 0; - private lastClientY = 0; + private dragState: DragState = { + isDragging: false, + lastPosition: { x: 0, y: 0 }, + pointerId: null, + }; ngOnDestroy(): void { this.removeDocumentListeners(); @@ -39,41 +55,74 @@ export class NgDiagramMinimapNavigationDirective implements OnDestroy { event.preventDefault(); - this.isDragging = true; - this.lastClientX = event.clientX; - this.lastClientY = event.clientY; - - document.addEventListener('pointermove', this.onPointerMove); - document.addEventListener('pointerup', this.onPointerUp); + this.capturePointer(event); + this.dragState.isDragging = true; + this.dragState.lastPosition = { x: event.clientX, y: event.clientY }; + this.attachDocumentListeners(); } private onPointerMove = (event: PointerEvent): void => { - if (!this.isDragging) { + if (!this.dragState.isDragging) { return; } - const deltaX = event.clientX - this.lastClientX; - const deltaY = event.clientY - this.lastClientY; - - this.lastClientX = event.clientX; - this.lastClientY = event.clientY; - - const transform = this.transform(); - const viewport = this.viewport(); - - const diagramDeltaX = deltaX / transform.scale; - const diagramDeltaY = deltaY / transform.scale; + const delta = this.calculateClientDelta(event); + this.dragState.lastPosition = { x: event.clientX, y: event.clientY }; - this.viewportService.moveViewportBy(-diagramDeltaX * viewport.scale, -diagramDeltaY * viewport.scale); + const viewportDelta = this.calculateViewportDelta(delta); + this.viewportService.moveViewportBy(viewportDelta.x, viewportDelta.y); }; - private onPointerUp = (): void => { - this.isDragging = false; + private onPointerUp = (event: PointerEvent): void => { + this.dragState.isDragging = false; + this.releasePointer(event); this.removeDocumentListeners(); }; + private capturePointer(event: PointerEvent): void { + const target = event.target as Element; + target.setPointerCapture(event.pointerId); + this.dragState.pointerId = event.pointerId; + } + + private releasePointer(event: PointerEvent): void { + if (this.dragState.pointerId === null) { + return; + } + + const target = event.target as Element; + if (target.hasPointerCapture(this.dragState.pointerId)) { + target.releasePointerCapture(this.dragState.pointerId); + } + this.dragState.pointerId = null; + } + + private attachDocumentListeners(): void { + document.addEventListener('pointermove', this.onPointerMove); + document.addEventListener('pointerup', this.onPointerUp); + document.addEventListener('pointercancel', this.onPointerUp); + } + private removeDocumentListeners(): void { document.removeEventListener('pointermove', this.onPointerMove); document.removeEventListener('pointerup', this.onPointerUp); + document.removeEventListener('pointercancel', this.onPointerUp); + } + + private calculateClientDelta(event: PointerEvent): Point { + return { + x: event.clientX - this.dragState.lastPosition.x, + y: event.clientY - this.dragState.lastPosition.y, + }; + } + + private calculateViewportDelta(clientDelta: Point): Point { + const { scale: minimapScale } = this.transform(); + const { scale: viewportScale } = this.viewport(); + + return { + x: -(clientDelta.x / minimapScale) * viewportScale, + y: -(clientDelta.y / minimapScale) * viewportScale, + }; } } diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.scss b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.scss index bf1064843..1b113413b 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.scss +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.scss @@ -28,6 +28,7 @@ .minimap-svg { display: block; cursor: pointer; + touch-action: none; } .minimap-background {