UNPKG

@progress/kendo-angular-diagrams

Version:

Kendo UI Angular diagrams component

656 lines (655 loc) 23.8 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { Component, ElementRef, EventEmitter, HostBinding, Input, NgZone, Output, Renderer2, } from '@angular/core'; import { Diagram } from '@progress/kendo-diagram-common'; import { validatePackage } from '@progress/kendo-licensing'; import { packageMetadata } from './package-metadata'; import { hasObservers, shouldShowValidationUI, WatermarkOverlayComponent } from '@progress/kendo-angular-common'; import { NgIf } from '@angular/common'; import { events, loadTheme } from '@progress/kendo-diagram-common'; import { CONNECTION_DEFAULTS, SHAPE_DEFAULTS } from './utils'; import * as i0 from "@angular/core"; /** * Represents the [Kendo UI Diagram component for Angular({% slug overview_diagrams %}). * * The Diagram component is used to create organizational charts, and other types of diagrams. * * @example * ```html * <kendo-diagram [shapes]="shapesData"></kendo-diagram> * ``` */ export class DiagramComponent { wrapperElement; renderer; zone; diagramClass = true; /** * Defines the default configuration options applicable to all connections. * Changing the property value dynamically triggers a reinitialization of the Diagram. */ connectionDefaults = CONNECTION_DEFAULTS; /** * Defines the connections that render between the shapes in the Diagram. * Changing this property dynamically reinitializes the Diagram. */ connections; /** * A set of settings to configure the Diagram behavior when the user attempts to drag, resize, or remove shapes. * Changing the property value dynamically triggers a reinitialization of the Diagram. * @default true */ editable = { drag: true, resize: true, remove: true }; /** * The layout of a diagram consists of arranging the shapes (sometimes also the connections) in some fashion in order to achieve an aesthetically pleasing experience to the user. * Changing the property value dynamically triggers a reinitialization of the Diagram. */ layout = { type: 'tree' }; /** * Defines the pannable options. Use this setting to disable Diagram pan or change the key that activates the pan behavior. * @default true */ pannable = true; /** * Defines the Diagram selection options. * * By default, you can select shapes in the Diagram in one of two ways: * - Clicking a single shape to select it and deselect any previously selected shapes. * - Holding the `Ctrl/Cmd on MacOS` key while clicking multiple shapes to select them and any other shapes between them. * * Use the `selectable` configuration to allow single selection only, enable selection by drawing a rectangular area with the mouse around shapes in the canvas, or disable selection altogether. * @default true */ selectable = true; /** * Defines the default configuration options applicable to all shapes. * Changing the property value dynamically triggers a reinitialization of the Diagram. */ shapeDefaults = SHAPE_DEFAULTS; /** * Defines the shapes that render in the Diagram. * Changing the property value dynamically triggers a reinitialization of the Diagram. */ shapes; /** * Defines the zoom level of the Diagram. * * @default 1 */ zoom = 1; /** * Defines the maximum zoom level of the Diagram. * * @default 2 */ zoomMax = 2; /** * Defines the minimum zoom level of the Diagram. * * @default 0.1 */ zoomMin = 0.1; /** * Defines the zoom rate of the Diagram. * * @default 0.1 */ zoomRate = 0.1; /** * Fires when a shape or connection is created or removed. */ change = new EventEmitter(); /** * Fires when the user clicks on a shape or a connection. */ diagramClick = new EventEmitter(); /** * Fires when the user drags an item. */ drag = new EventEmitter(); /** * Fires when the user stops dragging an item. */ dragEnd = new EventEmitter(); /** * Fires when the user starts dragging an item. */ dragStart = new EventEmitter(); /** * Fires when the location or size of an item are changed. */ shapeBoundsChange = new EventEmitter(); /** * Fires when the mouse pointer enters a shape or connection. */ mouseEnter = new EventEmitter(); /** * Fires when the mouse pointer leaves a shape or connection. */ mouseLeave = new EventEmitter(); /** * Fires when the user pans the Diagram. */ onPan = new EventEmitter(); /** * Fires when the user selects one or more items. */ onSelect = new EventEmitter(); /** * Fires when the diagram has finished zooming out. */ zoomEnd = new EventEmitter(); /** * Fires when the diagram starts zooming in or out. */ zoomStart = new EventEmitter(); /** * @hidden * Represents the Diagram instance, holding the core functionality of the Diagram. */ diagram; /** * The currently selected items in the Diagram. */ get selection() { return this.diagram?.select(); } /** * The actual shapes created by the Diagram. */ get diagramShapes() { return this.diagram?.shapes; } /** * The actual connections created by the Diagram. */ get diagramConnections() { return this.diagram?.connections; } /** * @hidden */ showLicenseWatermark = false; options = { shapes: this.shapes, connections: this.connections, selectable: this.selectable, editable: this.editable, zoom: this.zoom, zoomMax: this.zoomMax, zoomMin: this.zoomMin, zoomRate: this.zoomRate, shapeDefaults: this.shapeDefaults, connectionDefaults: this.connectionDefaults, layout: this.layout, }; constructor(wrapperElement, renderer, zone) { this.wrapperElement = wrapperElement; this.renderer = renderer; this.zone = zone; const isValid = validatePackage(packageMetadata); this.showLicenseWatermark = shouldShowValidationUI(isValid); } ngOnChanges(changes) { let recreate = false; if (changes['shapes']) { this.options.shapes = this.shapes; recreate = true; } if (changes['connections']) { this.options.connections = this.connections; recreate = true; } if (changes['connectionDefaults']) { this.options.connectionDefaults = { ...this.options.connectionDefaults, ...this.connectionDefaults }; recreate = true; } if (changes['shapeDefaults']) { this.options.shapeDefaults = { ...this.options.shapeDefaults, ...this.shapeDefaults }; recreate = true; } if (changes['editable'] && this.editable || typeof this.editable === 'boolean') { if (typeof this.editable === 'boolean') { if (this.editable) { this.options.editable = { drag: true, resize: true, remove: true }; this.options.shapeDefaults.editable = { drag: true, remove: true }; } else { this.options.editable = { drag: false, resize: false, remove: false }; this.options.shapeDefaults.editable = { drag: false, remove: false }; } } if (typeof this.editable === 'object') { this.options.editable = { drag: this.editable.drag, resize: this.editable.resize, remove: this.editable.remove }; this.options.shapeDefaults.editable = { drag: this.editable.drag, remove: this.editable.remove }; } recreate = true; } if (changes['zoomMax']) { this.updateOptions('zoomMax'); } if (changes['zoomMin']) { this.updateOptions('zoomMin'); } if (changes['zoomRate']) { this.updateOptions('zoomRate'); } if (changes['selectable']) { this.updateOptions('selectable'); } if (changes['layout']) { this.updateOptions('layout'); this.diagram?.layout(this.options.layout); } if (changes['pannable']) { this.updateOptions('pannable'); } if (changes['zoom']) { this.updateOptions('zoom'); this.diagram?.zoom(this.diagram.options.zoom); } if (recreate) { this.init(); } } ngAfterViewInit() { this.renderer.setStyle(this.wrapperElement.nativeElement, 'display', 'block'); this.init(); events.forEach((eventName) => { this.diagram.bind(eventName, (e) => { if (eventName === 'click') { eventName = 'diagramClick'; } if (eventName === 'select') { eventName = 'onSelect'; } if (eventName === 'pan') { eventName = 'onPan'; } if (eventName === 'itemBoundsChange') { eventName = 'shapeBoundsChange'; } const emitter = this.activeEmitter(eventName); if (emitter) { this.zone.run(() => { emitter.emit(e); }); } }); }); } ngOnDestroy() { this.diagram?.destroy(); } /** * Provides the current Diagram's shapes and connections that can be used to create a new Diagram when needed. * @returns {DiagramState} Object containing shapes and connections arrays. */ getState() { return this.diagram?.save(); } /** * Focuses the Diagram. * @returns {boolean} true if focus was set successfully. */ focus() { return this.diagram?.focus(); } /** * Clears the Diagram. */ clear() { this.diagram?.clear(); } /** * Determines whether two shapes are connected. * @param {Shape} Shape. * @param {Shape} Shape. * @returns {boolean} true if the two shapes are connected. */ connected(source, target) { return this.diagram?.connected(source, target); } /** * Adds connection to the Diagram. * @param {Connection} Connection. * @param {boolean} Boolean. * @returns {Connection} The newly created connection. */ addConnection(connection, undoable) { const newConnection = this.diagram?.addConnection(connection, undoable); return newConnection; } /** * Adds shape to the Diagram. * @param {ShapeOptions | Shape | Point} If you pass a `Point`, a new Shape with default options will be created and positioned at that point. * @param {boolean} Boolean indicating if the action should be undoable. * @returns {Shape} The newly created shape. */ addShape(item, undoable) { const newShape = this.diagram?.addShape(item, undoable); return newShape; } /** * Removes shape(s) and/or connection(s) from the Diagram. * @param {Shape | Connection | (Shape | Connection)[]} Shape, Connection or an Array of Shapes and/or Connections. * @param {Boolean} Boolean indicating if the action should be undoable. */ remove(items, undoable) { this.diagram?.remove(items, undoable); } /** * Connects two items in the Diagram. * @param {Shape | Connector | Point} Shape, Shape's Connector or Point. * @param {Shape | Connector | Point} Shape, Shape's Connector or Point. * @param {ConnectionOptions} Connection options. * @returns {Connection} The created connection. */ connect(source, target, options) { return this.diagram?.connect(source, target, options); } /** * Executes the next undoable action on top of the undo stack if any. */ undo() { this.diagram?.undo(); } /** * Executes the previous undoable action on top of the redo stack if any. */ redo() { this.diagram?.redo(); } /** * Selects items on the basis of the given input. * @param {Shape | Connection | (Shape | Connection)[]} Shape, Connection or an Array of Shapes and/or Connections. * @param Selection options. * @returns {(Shape | Connection)[]} Array of selected items. */ select(items, options) { return this.diagram?.select(items, options); } /** * Selects all items in the Diagram. */ selectAll() { this.diagram?.selectAll(); } /** * Selects items in the specified area. * @param {Rect} rect Rectangle area to select. */ selectArea(rect) { this.diagram?.selectArea(rect); } /** * Deselects the specified items or all items if no item is specified. * @param {Shape | Connection | (Shape | Connection)[]} Shape, Connection or an Array of Shapes and/or Connections. */ deselect(items) { this.diagram?.deselect(items); } /** * Brings to front the passed items. * @param {Shape | Connection | (Shape | Connection)[]} Shape, Connection or an Array of Shapes and/or Connections. * @param {boolean} By default the action is undoable. */ bringToFront(items, undoable) { this.diagram?.toFront(items, undoable); } /** * Sends to back the passed items. * @param {Shape | Connection | (Shape | Connection)[]} Shape, Connection or an Array of Shapes and/or Connections. * @param {boolean} By default the action is undoable. */ bringToBack(items, undoable) { this.diagram?.toBack(items, undoable); } /** * Bring into view the passed item(s) or rectangle. * @param {Shape | Connection | (Shape | Connection)[]} Shape, Connection or an Array of Shapes and/or Connections. * @param {DiagramAlignOptions} controls the position of the calculated rectangle relative to the viewport. * "Center middle" will position the items in the center. animate - controls if the pan should be animated. */ bringIntoView(item, options) { this.diagram?.bringIntoView(item, options); } /** * Aligns shapes in the specified direction. * @param {Direction} Direction to align shapes. */ alignShapes(direction) { this.diagram?.alignShapes(direction); } /** * @hidden * Pans the Diagram. * @param {Point} Pan coordinates. * @param {boolean} Whether to animate the pan. * @returns {Point} Current pan position. */ pan(pan, animate) { return this.diagram?.pan(pan, animate); } /** * Gets the current `Diagram` viewport rectangle. * @returns {Rect} Viewport rectangle. */ viewport() { return this.diagram?.viewport(); } /** * Copies selected items to clipboard. */ copy() { this.diagram?.copy(); } /** * @hidden * Cuts selected items to clipboard. */ cut() { this.diagram?.cut(); } /** * Pastes items from clipboard. */ paste() { this.diagram?.paste(); } /** * Gets the bounding rectangle of the given items. * @param {Shape | Connection | (Shape | Connection)[]} Shape, Connection or an Array of Shapes and/or Connections. * @param {boolean} Pass 'true' if you need to get the bounding box of the shapes without their rotation offset. * @returns {Rect} Bounding rectangle. */ boundingBox(items, origin) { return this.diagram?.boundingBox(items, origin); } /** * Converts document coordinates to view coordinates. * @param {Point} Point in document coordinates. * @returns {Point} Point in view coordinates. */ documentToView(point) { return this.diagram?.documentToView(point); } /** * Converts view coordinates to document coordinates. * @param {Point} Point in view coordinates. * @returns {Point} Point in document coordinates. */ viewToDocument(point) { return this.diagram?.viewToDocument(point); } /** * Converts view coordinates to model coordinates. * @param {Point} Point in view coordinates. * @returns {Point} Point in model coordinates. */ viewToModel(point) { return this.diagram?.viewToModel(point); } /** * Converts model coordinates to view coordinates. * @param {Point} Point in model coordinates. * @returns {Point} Point in view coordinates. */ modelToView(point) { return this.diagram?.modelToView(point); } /** * Converts model coordinates to layer coordinates. * @param {Point} Point in model coordinates. * @returns {Point} Point in layer coordinates. */ modelToLayer(point) { return this.diagram?.modelToLayer(point); } /** * Converts layer coordinates to model coordinates. * @param {Point} Point in layer coordinates. * @returns {Point} Point in model coordinates. */ layerToModel(point) { return this.diagram?.layerToModel(point); } /** * Converts document coordinates to model coordinates. * @param {Point} Point in document coordinates. * @returns {Point} Point in model coordinates. */ documentToModel(point) { return this.diagram?.documentToModel(point); } /** * Converts model coordinates to document coordinates. * @param {Point} Point in model coordinates. * @returns {Point} Point in document coordinates. */ modelToDocument(point) { return this.diagram?.modelToDocument(point); } /** * Gets a shape on the basis of its identifier. * @param {string} The identifier of a shape. * @returns {Shape} The shape with the specified ID. */ getShapeById(id) { return this.diagram?.getShapeById(id); } /** * Exports the diagram's DOM visual representation for rendering or export purposes. * Creates a clipped group containing the canvas content with proper transformations. * @returns {Group} A drawing Group element containing the exported DOM visual */ exportDOMVisual() { return this.diagram?.exportDOMVisual(); } /** * Exports the diagram's visual representation with proper scaling based on zoom level. * Creates a scaled group containing the main layer content. * @returns {Group} A drawing Group element containing the exported visual with inverse zoom scaling */ exportVisual() { return this.diagram?.exportVisual(); } activeEmitter(name) { const emitter = this[name]; if (emitter?.emit && hasObservers(emitter)) { return emitter; } } init() { if (this.diagram) { this.diagram.destroy(); } this.zone.runOutsideAngular(() => { const theme = loadTheme(this.wrapperElement.nativeElement); this.diagram = new Diagram(this.wrapperElement.nativeElement, this.options, theme); this.diagram._createOptionElements(); this.diagram.zoom(this.diagram.options.zoom); this.diagram.canvas.draw(); this.diagram.bringIntoView(this.diagram.shapes); }); } updateOptions(prop) { this.options[prop] = this[prop]; this.diagram?.setOptions(this.options); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DiagramComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: DiagramComponent, isStandalone: true, selector: "kendo-diagram", inputs: { connectionDefaults: "connectionDefaults", connections: "connections", editable: "editable", layout: "layout", pannable: "pannable", selectable: "selectable", shapeDefaults: "shapeDefaults", shapes: "shapes", zoom: "zoom", zoomMax: "zoomMax", zoomMin: "zoomMin", zoomRate: "zoomRate" }, outputs: { change: "change", diagramClick: "diagramClick", drag: "drag", dragEnd: "dragEnd", dragStart: "dragStart", shapeBoundsChange: "shapeBoundsChange", mouseEnter: "mouseEnter", mouseLeave: "mouseLeave", onPan: "pan", onSelect: "select", zoomEnd: "zoomEnd", zoomStart: "zoomStart" }, host: { properties: { "class.k-diagram": "this.diagramClass" } }, exportAs: ["kendoDiagram"], usesOnChanges: true, ngImport: i0, template: ` <div kendoWatermarkOverlay *ngIf="showLicenseWatermark"></div> `, isInline: true, dependencies: [{ kind: "component", type: WatermarkOverlayComponent, selector: "div[kendoWatermarkOverlay]" }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DiagramComponent, decorators: [{ type: Component, args: [{ standalone: true, exportAs: 'kendoDiagram', selector: 'kendo-diagram', template: ` <div kendoWatermarkOverlay *ngIf="showLicenseWatermark"></div> `, imports: [WatermarkOverlayComponent, NgIf] }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i0.NgZone }]; }, propDecorators: { diagramClass: [{ type: HostBinding, args: ['class.k-diagram'] }], connectionDefaults: [{ type: Input }], connections: [{ type: Input }], editable: [{ type: Input }], layout: [{ type: Input }], pannable: [{ type: Input }], selectable: [{ type: Input }], shapeDefaults: [{ type: Input }], shapes: [{ type: Input }], zoom: [{ type: Input }], zoomMax: [{ type: Input }], zoomMin: [{ type: Input }], zoomRate: [{ type: Input }], change: [{ type: Output }], diagramClick: [{ type: Output }], drag: [{ type: Output }], dragEnd: [{ type: Output }], dragStart: [{ type: Output }], shapeBoundsChange: [{ type: Output }], mouseEnter: [{ type: Output }], mouseLeave: [{ type: Output }], onPan: [{ type: Output, args: ['pan'] }], onSelect: [{ type: Output, args: ['select'] }], zoomEnd: [{ type: Output }], zoomStart: [{ type: Output }] } });