@ship-ui/core 0.15.18 → 0.15.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/ship-ui-core.mjs +1091 -25
- package/fesm2022/ship-ui-core.mjs.map +1 -1
- package/index.d.ts +101 -4
- package/package.json +4 -1
- package/styles/components/ship-blueprint.component.scss +238 -0
- package/styles/index.scss +4 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, ElementRef, Renderer2, ChangeDetectionStrategy, Component, signal, DestroyRef, InjectionToken, input, computed, viewChild, effect, HostListener, NgModule, Injectable, DOCUMENT, model, output, ApplicationRef, createComponent, isSignal, OutputEmitterRef, contentChild, contentChildren, afterNextRender, assertInInjectionContext, Injector, HostBinding, TemplateRef, runInInjectionContext, Directive, ChangeDetectorRef, viewChildren, ViewContainerRef, EnvironmentInjector } from '@angular/core';
|
|
3
|
-
import { DatePipe, NgTemplateOutlet } from '@angular/common';
|
|
2
|
+
import { inject, ElementRef, Renderer2, ChangeDetectionStrategy, Component, signal, DestroyRef, InjectionToken, input, computed, viewChild, effect, HostListener, NgModule, Injectable, DOCUMENT, model, output, ApplicationRef, createComponent, isSignal, OutputEmitterRef, PLATFORM_ID, ViewChild, contentChild, contentChildren, afterNextRender, assertInInjectionContext, Injector, HostBinding, TemplateRef, runInInjectionContext, Directive, ChangeDetectorRef, viewChildren, ViewContainerRef, EnvironmentInjector } from '@angular/core';
|
|
3
|
+
import { isPlatformBrowser, JsonPipe, DatePipe, NgTemplateOutlet } from '@angular/common';
|
|
4
|
+
import { ShipCardComponent as ShipCardComponent$1, ShipIconComponent as ShipIconComponent$1, ShipButtonComponent as ShipButtonComponent$1 } from 'ship-ui';
|
|
4
5
|
import { NgModel } from '@angular/forms';
|
|
5
6
|
import { SIGNAL } from '@angular/core/primitives/signals';
|
|
6
7
|
|
|
@@ -35,8 +36,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImpor
|
|
|
35
36
|
}]
|
|
36
37
|
}] });
|
|
37
38
|
|
|
38
|
-
function classMutationSignal() {
|
|
39
|
-
const element = inject(ElementRef).nativeElement;
|
|
39
|
+
function classMutationSignal(_element = null) {
|
|
40
|
+
const element = _element ?? inject(ElementRef).nativeElement;
|
|
40
41
|
if (!element)
|
|
41
42
|
return signal('');
|
|
42
43
|
const classListSignal = signal(element.className, ...(ngDevMode ? [{ debugName: "classListSignal" }] : []));
|
|
@@ -476,9 +477,10 @@ class ShipDialogService {
|
|
|
476
477
|
open(component, options) {
|
|
477
478
|
const environmentInjector = this.#appRef.injector;
|
|
478
479
|
const hostElement = this.#createEl();
|
|
480
|
+
let closingCalled = false;
|
|
479
481
|
const { data, closed, ...rest } = options || {};
|
|
480
482
|
if (this.compRef) {
|
|
481
|
-
this.#cleanupRefs();
|
|
483
|
+
this.#cleanupRefs(true);
|
|
482
484
|
}
|
|
483
485
|
this.insertedCompRef = createComponent(component, {
|
|
484
486
|
environmentInjector,
|
|
@@ -500,8 +502,12 @@ class ShipDialogService {
|
|
|
500
502
|
}
|
|
501
503
|
if (closedField instanceof OutputEmitterRef) {
|
|
502
504
|
this.closedFieldSub = closedField.subscribe((...args) => {
|
|
503
|
-
closed?.(...args);
|
|
504
505
|
this.#cleanupRefs();
|
|
506
|
+
if (closingCalled)
|
|
507
|
+
return;
|
|
508
|
+
closingCalled = true;
|
|
509
|
+
closed?.(...args);
|
|
510
|
+
this.compRef?.instance.closed.emit(...args);
|
|
505
511
|
});
|
|
506
512
|
}
|
|
507
513
|
this.#appRef.attachView(this.insertedCompRef.hostView);
|
|
@@ -510,20 +516,22 @@ class ShipDialogService {
|
|
|
510
516
|
this.compRef.changeDetectorRef.detectChanges();
|
|
511
517
|
this.compRef.instance.isOpen.set(true);
|
|
512
518
|
this.compRef.setInput('options', rest);
|
|
513
|
-
this.compRef.instance.closed.subscribe(() => closeAction());
|
|
519
|
+
this.compClosedSub = this.compRef.instance.closed.subscribe(() => closeAction());
|
|
514
520
|
const _self = this;
|
|
515
|
-
function closeAction() {
|
|
521
|
+
function closeAction(arg = undefined) {
|
|
522
|
+
_self.#cleanupRefs();
|
|
523
|
+
if (closingCalled)
|
|
524
|
+
return;
|
|
525
|
+
closingCalled = true;
|
|
516
526
|
if (closedField && closedField instanceof OutputEmitterRef) {
|
|
517
|
-
closedField.emit(
|
|
518
|
-
}
|
|
519
|
-
else {
|
|
520
|
-
closed?.(undefined);
|
|
527
|
+
closedField.emit(arg);
|
|
521
528
|
}
|
|
522
|
-
|
|
529
|
+
closed?.(arg);
|
|
523
530
|
}
|
|
524
531
|
return {
|
|
525
532
|
component: this.insertedCompRef.instance,
|
|
526
533
|
close: closeAction,
|
|
534
|
+
closed: this.compRef.instance.closed,
|
|
527
535
|
};
|
|
528
536
|
}
|
|
529
537
|
#createEl() {
|
|
@@ -534,20 +542,24 @@ class ShipDialogService {
|
|
|
534
542
|
}
|
|
535
543
|
return this.#document.getElementById('sh-dialog-ref');
|
|
536
544
|
}
|
|
537
|
-
#cleanupRefs() {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
545
|
+
#cleanupRefs(instant = false) {
|
|
546
|
+
const _self = this;
|
|
547
|
+
instant ? cleanup : queueMicrotask(() => cleanup());
|
|
548
|
+
function cleanup() {
|
|
549
|
+
if (_self.insertedCompRef) {
|
|
550
|
+
_self.#appRef.detachView(_self.insertedCompRef.hostView);
|
|
551
|
+
_self.closedFieldSub?.unsubscribe();
|
|
552
|
+
_self.insertedCompRef.destroy();
|
|
553
|
+
}
|
|
554
|
+
if (!_self.compRef)
|
|
555
|
+
return;
|
|
556
|
+
_self.#appRef.detachView(_self.compRef.hostView);
|
|
557
|
+
_self.compClosedSub?.unsubscribe();
|
|
558
|
+
_self.compRef.destroy();
|
|
542
559
|
}
|
|
543
|
-
if (!this.compRef)
|
|
544
|
-
return;
|
|
545
|
-
this.#appRef.detachView(this.compRef.hostView);
|
|
546
|
-
this.compClosedSub?.unsubscribe();
|
|
547
|
-
this.compRef.destroy();
|
|
548
560
|
}
|
|
549
561
|
ngOnDestroy() {
|
|
550
|
-
this.#cleanupRefs();
|
|
562
|
+
this.#cleanupRefs(true);
|
|
551
563
|
}
|
|
552
564
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ShipDialogService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
553
565
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ShipDialogService, providedIn: 'root' }); }
|
|
@@ -559,6 +571,1060 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImpor
|
|
|
559
571
|
}]
|
|
560
572
|
}] });
|
|
561
573
|
|
|
574
|
+
function layoutNodes(nodes) {
|
|
575
|
+
if (!nodes || nodes.length === 0)
|
|
576
|
+
return nodes;
|
|
577
|
+
const layoutedNodes = structuredClone(nodes);
|
|
578
|
+
const visited = new Set();
|
|
579
|
+
const unconnectedNodes = [];
|
|
580
|
+
const connectedGraphs = [];
|
|
581
|
+
const allToNodes = new Set(layoutedNodes.flatMap((node) => node.connections).map((conn) => conn.toNode));
|
|
582
|
+
const rootNodes = layoutedNodes.filter((node) => !allToNodes.has(node.id));
|
|
583
|
+
for (const node of layoutedNodes) {
|
|
584
|
+
if (!node.connections.length) {
|
|
585
|
+
unconnectedNodes.push(node);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
for (const root of rootNodes) {
|
|
589
|
+
if (!visited.has(root.id)) {
|
|
590
|
+
const graph = [];
|
|
591
|
+
traverseGraph(root, graph, visited, layoutedNodes);
|
|
592
|
+
if (graph.length > 0) {
|
|
593
|
+
connectedGraphs.push(graph);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
positionUnconnectedNodes(unconnectedNodes);
|
|
598
|
+
let currentX = 20;
|
|
599
|
+
let currentY = 20;
|
|
600
|
+
connectedGraphs.forEach((graph) => {
|
|
601
|
+
const layoutedGraph = traverseAndLayout(graph);
|
|
602
|
+
positionGraph(layoutedGraph, currentX, currentY);
|
|
603
|
+
currentY += getGraphHeight(layoutedGraph) + 200;
|
|
604
|
+
});
|
|
605
|
+
return layoutedNodes;
|
|
606
|
+
}
|
|
607
|
+
function traverseGraph(startNode, graph, visited, allNodes) {
|
|
608
|
+
const queue = [startNode];
|
|
609
|
+
visited.add(startNode.id);
|
|
610
|
+
let head = 0;
|
|
611
|
+
while (head < queue.length) {
|
|
612
|
+
const node = queue[head++];
|
|
613
|
+
graph.push(node);
|
|
614
|
+
node.connections.forEach((conn) => {
|
|
615
|
+
const toNode = allNodes.find((n) => n.id === conn.toNode);
|
|
616
|
+
if (toNode && !visited.has(toNode.id)) {
|
|
617
|
+
visited.add(toNode.id);
|
|
618
|
+
queue.push(toNode);
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function positionUnconnectedNodes(nodes) {
|
|
624
|
+
nodes.forEach((node, index) => {
|
|
625
|
+
node.coordinates[0] = 20 + index * 200;
|
|
626
|
+
node.coordinates[1] = 20;
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
function traverseAndLayout(graph) {
|
|
630
|
+
const graphNodeMap = new Map(graph.map((n) => [n.id, n]));
|
|
631
|
+
const incomingConnectionCount = new Map();
|
|
632
|
+
graph.forEach((node) => {
|
|
633
|
+
incomingConnectionCount.set(node.id, 0);
|
|
634
|
+
});
|
|
635
|
+
graph
|
|
636
|
+
.flatMap((n) => n.connections)
|
|
637
|
+
.forEach((conn) => {
|
|
638
|
+
if (graphNodeMap.has(conn.toNode)) {
|
|
639
|
+
incomingConnectionCount.set(conn.toNode, (incomingConnectionCount.get(conn.toNode) || 0) + 1);
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
const roots = graph.filter((node) => (incomingConnectionCount.get(node.id) || 0) === 0);
|
|
643
|
+
const layoutedNodes = new Set();
|
|
644
|
+
const queue = [...roots];
|
|
645
|
+
let head = 0;
|
|
646
|
+
while (head < queue.length) {
|
|
647
|
+
const currentNode = queue[head++];
|
|
648
|
+
layoutedNodes.add(currentNode.id);
|
|
649
|
+
let outputConnectionIndex = 0;
|
|
650
|
+
const connectedNodes = [];
|
|
651
|
+
currentNode.connections.forEach((conn) => {
|
|
652
|
+
const toNode = graphNodeMap.get(conn.toNode);
|
|
653
|
+
if (toNode && !layoutedNodes.has(toNode.id)) {
|
|
654
|
+
connectedNodes.push(toNode);
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
connectedNodes.forEach((toNode) => {
|
|
658
|
+
toNode.coordinates[0] = currentNode.coordinates[0] + 200;
|
|
659
|
+
if (connectedNodes.length > 1) {
|
|
660
|
+
const verticalOffset = (outputConnectionIndex - (connectedNodes.length - 1) / 2) * 200;
|
|
661
|
+
toNode.coordinates[1] = currentNode.coordinates[1] + verticalOffset;
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
toNode.coordinates[1] = currentNode.coordinates[1];
|
|
665
|
+
}
|
|
666
|
+
queue.push(toNode);
|
|
667
|
+
outputConnectionIndex++;
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
return graph;
|
|
671
|
+
}
|
|
672
|
+
function positionGraph(graph, startX, startY) {
|
|
673
|
+
const minX = Math.min(...graph.map((n) => n.coordinates[0]));
|
|
674
|
+
const minY = Math.min(...graph.map((n) => n.coordinates[1]));
|
|
675
|
+
graph.forEach((node) => {
|
|
676
|
+
node.coordinates[0] += startX - minX;
|
|
677
|
+
node.coordinates[1] += startY - minY;
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
function getGraphHeight(graph) {
|
|
681
|
+
if (!graph.length)
|
|
682
|
+
return 0;
|
|
683
|
+
const maxY = Math.max(...graph.map((n) => n.coordinates[1]));
|
|
684
|
+
const minY = Math.min(...graph.map((n) => n.coordinates[1]));
|
|
685
|
+
return maxY - minY;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function findDuplicatePortIDs(nodes) {
|
|
689
|
+
const errors = [];
|
|
690
|
+
for (const node of nodes) {
|
|
691
|
+
const portIdCounts = new Map();
|
|
692
|
+
const allPorts = [...node.inputs, ...node.outputs];
|
|
693
|
+
for (const port of allPorts) {
|
|
694
|
+
const count = portIdCounts.get(port.id) || 0;
|
|
695
|
+
portIdCounts.set(port.id, count + 1);
|
|
696
|
+
}
|
|
697
|
+
const duplicatePortIds = [];
|
|
698
|
+
for (const [id, count] of portIdCounts.entries()) {
|
|
699
|
+
if (count > 1) {
|
|
700
|
+
duplicatePortIds.push(id);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (duplicatePortIds.length > 0) {
|
|
704
|
+
errors.push({
|
|
705
|
+
nodeId: node.id,
|
|
706
|
+
duplicatePortIds: duplicatePortIds,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return errors;
|
|
711
|
+
}
|
|
712
|
+
function findDuplicateNodeIDs(nodes) {
|
|
713
|
+
const nodeIdCounts = new Map();
|
|
714
|
+
for (const node of nodes) {
|
|
715
|
+
const count = nodeIdCounts.get(node.id) || 0;
|
|
716
|
+
nodeIdCounts.set(node.id, count + 1);
|
|
717
|
+
}
|
|
718
|
+
const duplicateNodeIds = [];
|
|
719
|
+
for (const [id, count] of nodeIdCounts.entries()) {
|
|
720
|
+
if (count > 1) {
|
|
721
|
+
duplicateNodeIds.push(id);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return duplicateNodeIds;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const TEST_NODES = [
|
|
728
|
+
{
|
|
729
|
+
id: 'a1',
|
|
730
|
+
name: 'hello a1',
|
|
731
|
+
coordinates: [0, 0],
|
|
732
|
+
inputs: [],
|
|
733
|
+
outputs: [{ id: 'out-1', name: 'Start Output' }],
|
|
734
|
+
connections: [{ fromNode: 'a1', fromPort: 'out-1', toNode: 'b1', toPort: 'in-1' }],
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
id: 'b1',
|
|
738
|
+
coordinates: [0, 0],
|
|
739
|
+
inputs: [
|
|
740
|
+
{ id: 'in-1', name: 'Input A' },
|
|
741
|
+
{ id: 'in-1', name: 'Another Input A' },
|
|
742
|
+
],
|
|
743
|
+
outputs: [{ id: 'out-1', name: 'Output B' }],
|
|
744
|
+
connections: [
|
|
745
|
+
{ fromNode: 'b1', fromPort: 'out-1', toNode: 'c6', toPort: 'in-1' },
|
|
746
|
+
{ fromNode: 'a1', fromPort: 'out-1', toNode: 'b1', toPort: 'in-1' },
|
|
747
|
+
],
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
id: 'c6',
|
|
751
|
+
name: 'hello c6',
|
|
752
|
+
coordinates: [0, 0],
|
|
753
|
+
inputs: [{ id: 'in-1', name: 'Input C' }],
|
|
754
|
+
outputs: [{ id: 'out-1', name: 'Output D' }],
|
|
755
|
+
connections: [{ fromNode: 'b1', fromPort: 'out-1', toNode: 'c6', toPort: 'in-1' }],
|
|
756
|
+
},
|
|
757
|
+
];
|
|
758
|
+
class ShipBlueprintComponent {
|
|
759
|
+
#ZOOM_SPEED;
|
|
760
|
+
#MAX_ZOOM;
|
|
761
|
+
#MIN_ZOOM;
|
|
762
|
+
#document;
|
|
763
|
+
#platformId;
|
|
764
|
+
#selfRef;
|
|
765
|
+
#currentClass;
|
|
766
|
+
#htmlClass;
|
|
767
|
+
#currentGridColor;
|
|
768
|
+
#draggingNodeCoordinates;
|
|
769
|
+
#isDragging;
|
|
770
|
+
#lastMouseX;
|
|
771
|
+
#lastMouseY;
|
|
772
|
+
#initialPinchDistance;
|
|
773
|
+
#isNodeDragging;
|
|
774
|
+
#draggedNodeId;
|
|
775
|
+
#dragOffset;
|
|
776
|
+
#connections;
|
|
777
|
+
#ctx;
|
|
778
|
+
#resizeObserver;
|
|
779
|
+
constructor() {
|
|
780
|
+
this.#ZOOM_SPEED = 0.01;
|
|
781
|
+
this.#MAX_ZOOM = 1.5;
|
|
782
|
+
this.#MIN_ZOOM = 0.5;
|
|
783
|
+
this.#document = inject(DOCUMENT);
|
|
784
|
+
this.#platformId = inject(PLATFORM_ID);
|
|
785
|
+
this.#selfRef = inject((ElementRef));
|
|
786
|
+
this.#currentClass = classMutationSignal();
|
|
787
|
+
this.#htmlClass = classMutationSignal(this.#document.documentElement);
|
|
788
|
+
this.asDots = computed(() => this.#currentClass().includes('dots'), ...(ngDevMode ? [{ debugName: "asDots" }] : []));
|
|
789
|
+
this.lightMode = computed(() => this.#htmlClass().includes('light'), ...(ngDevMode ? [{ debugName: "lightMode" }] : []));
|
|
790
|
+
this.forceUnique = input(true, ...(ngDevMode ? [{ debugName: "forceUnique" }] : []));
|
|
791
|
+
this.autoLayout = input(false, ...(ngDevMode ? [{ debugName: "autoLayout" }] : []));
|
|
792
|
+
this.gridSize = input(20, ...(ngDevMode ? [{ debugName: "gridSize" }] : []));
|
|
793
|
+
this.snapToGrid = input(true, ...(ngDevMode ? [{ debugName: "snapToGrid" }] : []));
|
|
794
|
+
this.gridColor = input(['#d8d8d8', '#2c2c2c'], ...(ngDevMode ? [{ debugName: "gridColor" }] : []));
|
|
795
|
+
this.nodes = model([], ...(ngDevMode ? [{ debugName: "nodes" }] : []));
|
|
796
|
+
this.#currentGridColor = computed(() => this.gridColor()[this.lightMode() ? 0 : 1], ...(ngDevMode ? [{ debugName: "#currentGridColor" }] : []));
|
|
797
|
+
this.panX = signal(0, ...(ngDevMode ? [{ debugName: "panX" }] : []));
|
|
798
|
+
this.panY = signal(0, ...(ngDevMode ? [{ debugName: "panY" }] : []));
|
|
799
|
+
this.zoomLevel = signal(1, ...(ngDevMode ? [{ debugName: "zoomLevel" }] : []));
|
|
800
|
+
this.gridSnapSize = signal(20, ...(ngDevMode ? [{ debugName: "gridSnapSize" }] : []));
|
|
801
|
+
this.isHoveringNode = signal(false, ...(ngDevMode ? [{ debugName: "isHoveringNode" }] : []));
|
|
802
|
+
this.midpointDivPosition = signal(null, ...(ngDevMode ? [{ debugName: "midpointDivPosition" }] : []));
|
|
803
|
+
this.showMidpointDiv = signal(false, ...(ngDevMode ? [{ debugName: "showMidpointDiv" }] : []));
|
|
804
|
+
this.isLocked = signal(false, ...(ngDevMode ? [{ debugName: "isLocked" }] : []));
|
|
805
|
+
this.draggingConnection = signal(null, ...(ngDevMode ? [{ debugName: "draggingConnection" }] : []));
|
|
806
|
+
this.validationErrors = signal(null, ...(ngDevMode ? [{ debugName: "validationErrors" }] : []));
|
|
807
|
+
this.highlightedConnection = signal(null, ...(ngDevMode ? [{ debugName: "highlightedConnection" }] : []));
|
|
808
|
+
this.#draggingNodeCoordinates = signal(null, ...(ngDevMode ? [{ debugName: "#draggingNodeCoordinates" }] : []));
|
|
809
|
+
this.#isDragging = signal(false, ...(ngDevMode ? [{ debugName: "#isDragging" }] : []));
|
|
810
|
+
this.#lastMouseX = signal(0, ...(ngDevMode ? [{ debugName: "#lastMouseX" }] : []));
|
|
811
|
+
this.#lastMouseY = signal(0, ...(ngDevMode ? [{ debugName: "#lastMouseY" }] : []));
|
|
812
|
+
this.#initialPinchDistance = signal(0, ...(ngDevMode ? [{ debugName: "#initialPinchDistance" }] : []));
|
|
813
|
+
this.#isNodeDragging = signal(false, ...(ngDevMode ? [{ debugName: "#isNodeDragging" }] : []));
|
|
814
|
+
this.#draggedNodeId = signal(null, ...(ngDevMode ? [{ debugName: "#draggedNodeId" }] : []));
|
|
815
|
+
this.#dragOffset = signal(null, ...(ngDevMode ? [{ debugName: "#dragOffset" }] : []));
|
|
816
|
+
this.#connections = signal([], ...(ngDevMode ? [{ debugName: "#connections" }] : []));
|
|
817
|
+
if (isPlatformBrowser(this.#platformId)) {
|
|
818
|
+
effect(() => {
|
|
819
|
+
this.asDots();
|
|
820
|
+
this.panX();
|
|
821
|
+
this.panY();
|
|
822
|
+
this.zoomLevel();
|
|
823
|
+
this.nodes();
|
|
824
|
+
this.#connections();
|
|
825
|
+
this.draggingConnection();
|
|
826
|
+
this.#currentGridColor();
|
|
827
|
+
this.highlightedConnection();
|
|
828
|
+
this.#draggingNodeCoordinates();
|
|
829
|
+
requestAnimationFrame(() => this.drawCanvas());
|
|
830
|
+
});
|
|
831
|
+
effect(() => {
|
|
832
|
+
const nodes = this.nodes();
|
|
833
|
+
const connectionsFromNodes = nodes.flatMap((node) => node.connections);
|
|
834
|
+
const uniqueConnections = connectionsFromNodes.filter((conn, index, self) => index ===
|
|
835
|
+
self.findIndex((c) => c.fromNode === conn.fromNode &&
|
|
836
|
+
c.fromPort === conn.fromPort &&
|
|
837
|
+
c.toNode === conn.toNode &&
|
|
838
|
+
c.toPort === conn.toPort));
|
|
839
|
+
this.#connections.set(uniqueConnections);
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
ngAfterViewInit() {
|
|
844
|
+
if (isPlatformBrowser(this.#platformId)) {
|
|
845
|
+
const canvas = this.canvasRef.nativeElement;
|
|
846
|
+
this.#ctx = canvas.getContext('2d');
|
|
847
|
+
this.#resizeObserver = new ResizeObserver(() => this.updateCanvasSize());
|
|
848
|
+
this.#resizeObserver.observe(this.#selfRef.nativeElement);
|
|
849
|
+
this.updateCanvasSize();
|
|
850
|
+
if (this.autoLayout()) {
|
|
851
|
+
this.applyAutolayout();
|
|
852
|
+
}
|
|
853
|
+
if (this.forceUnique()) {
|
|
854
|
+
this.#removeDuplicatesAndReinitiate();
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
this.#validateNodes();
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
ngOnDestroy() {
|
|
862
|
+
if (this.#resizeObserver) {
|
|
863
|
+
this.#resizeObserver.disconnect();
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
#validateNodes() {
|
|
867
|
+
const duplicatePortIds = findDuplicatePortIDs(this.nodes());
|
|
868
|
+
const duplicateNodeIds = findDuplicateNodeIDs(this.nodes());
|
|
869
|
+
this.validationErrors.set({
|
|
870
|
+
duplicateNodeIds,
|
|
871
|
+
duplicatePortIds,
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
applyAutolayout() {
|
|
875
|
+
const newNodes = layoutNodes(this.nodes());
|
|
876
|
+
this.nodes.set(newNodes);
|
|
877
|
+
}
|
|
878
|
+
updateCanvasSize() {
|
|
879
|
+
const canvas = this.canvasRef.nativeElement;
|
|
880
|
+
const rect = this.#selfRef.nativeElement.getBoundingClientRect();
|
|
881
|
+
const dpr = window.devicePixelRatio || 1;
|
|
882
|
+
canvas.width = rect.width * dpr;
|
|
883
|
+
canvas.height = rect.height * dpr;
|
|
884
|
+
this.#ctx.scale(dpr, dpr);
|
|
885
|
+
canvas.style.width = `${rect.width}px`;
|
|
886
|
+
canvas.style.height = `${rect.height}px`;
|
|
887
|
+
this.drawCanvas();
|
|
888
|
+
}
|
|
889
|
+
drawCanvas() {
|
|
890
|
+
if (!this.#ctx)
|
|
891
|
+
return;
|
|
892
|
+
const ctx = this.#ctx;
|
|
893
|
+
const { width, height } = this.canvasRef.nativeElement;
|
|
894
|
+
const dpr = window.devicePixelRatio || 1;
|
|
895
|
+
ctx.clearRect(0, 0, width / dpr, height / dpr);
|
|
896
|
+
ctx.save();
|
|
897
|
+
ctx.translate(this.panX(), this.panY());
|
|
898
|
+
ctx.scale(this.zoomLevel(), this.zoomLevel());
|
|
899
|
+
this.drawGrid(ctx);
|
|
900
|
+
this.drawConnections(ctx);
|
|
901
|
+
if (this.draggingConnection()) {
|
|
902
|
+
this.drawDraggingPath(ctx);
|
|
903
|
+
}
|
|
904
|
+
ctx.restore();
|
|
905
|
+
}
|
|
906
|
+
drawGrid(ctx) {
|
|
907
|
+
const { width, height } = this.canvasRef.nativeElement;
|
|
908
|
+
const dpr = window.devicePixelRatio || 1;
|
|
909
|
+
const zoom = this.zoomLevel();
|
|
910
|
+
const panX = this.panX();
|
|
911
|
+
const panY = this.panY();
|
|
912
|
+
const gridSize = this.gridSize();
|
|
913
|
+
const gridColor = this.#currentGridColor();
|
|
914
|
+
const scaledGridSize = gridSize * zoom;
|
|
915
|
+
const dynamicGridSize = scaledGridSize < 20 ? gridSize * 4 : gridSize;
|
|
916
|
+
const startX = Math.floor(-panX / zoom / dynamicGridSize) * dynamicGridSize;
|
|
917
|
+
const startY = Math.floor(-panY / zoom / dynamicGridSize) * dynamicGridSize;
|
|
918
|
+
const endX = startX + width / dpr / zoom + dynamicGridSize;
|
|
919
|
+
const endY = startY + height / dpr / zoom + dynamicGridSize;
|
|
920
|
+
if (this.asDots()) {
|
|
921
|
+
const dotRadius = 1 / zoom;
|
|
922
|
+
ctx.fillStyle = gridColor;
|
|
923
|
+
for (let x = startX; x < endX; x += dynamicGridSize) {
|
|
924
|
+
for (let y = startY; y < endY; y += dynamicGridSize) {
|
|
925
|
+
ctx.beginPath();
|
|
926
|
+
ctx.arc(x, y, dotRadius, 0, 2 * Math.PI);
|
|
927
|
+
ctx.fill();
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
932
|
+
ctx.beginPath();
|
|
933
|
+
ctx.strokeStyle = gridColor;
|
|
934
|
+
ctx.lineWidth = 1 / zoom;
|
|
935
|
+
for (let x = startX; x < endX; x += dynamicGridSize) {
|
|
936
|
+
ctx.moveTo(x, startY);
|
|
937
|
+
ctx.lineTo(x, endY);
|
|
938
|
+
}
|
|
939
|
+
for (let y = startY; y < endY; y += dynamicGridSize) {
|
|
940
|
+
ctx.moveTo(startX, y);
|
|
941
|
+
ctx.lineTo(endX, y);
|
|
942
|
+
}
|
|
943
|
+
ctx.stroke();
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
drawConnections(ctx) {
|
|
947
|
+
const highlighted = this.highlightedConnection();
|
|
948
|
+
ctx.lineWidth = 2 / this.zoomLevel();
|
|
949
|
+
for (const conn of this.#connections()) {
|
|
950
|
+
const isHighlighted = highlighted?.fromNode === conn.fromNode &&
|
|
951
|
+
highlighted?.fromPort === conn.fromPort &&
|
|
952
|
+
highlighted?.toNode === conn.toNode &&
|
|
953
|
+
highlighted?.toPort === conn.toPort;
|
|
954
|
+
ctx.strokeStyle = isHighlighted ? '#ffc107' : '#888';
|
|
955
|
+
ctx.beginPath();
|
|
956
|
+
const startPos = this.getNodePortPosition(conn.fromNode, conn.fromPort);
|
|
957
|
+
const endPos = this.getNodePortPosition(conn.toNode, conn.toPort);
|
|
958
|
+
this.drawCurvedPath(ctx, startPos, endPos);
|
|
959
|
+
ctx.stroke();
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
drawDraggingPath(ctx) {
|
|
963
|
+
const conn = this.draggingConnection();
|
|
964
|
+
const startPos = this.getNodePortPosition(conn.fromNode, conn.fromPort);
|
|
965
|
+
const endPos = [conn.x2, conn.y2];
|
|
966
|
+
ctx.strokeStyle = '#5a9cf8';
|
|
967
|
+
ctx.lineWidth = 2 / this.zoomLevel();
|
|
968
|
+
ctx.beginPath();
|
|
969
|
+
this.drawCurvedPath(ctx, startPos, endPos);
|
|
970
|
+
ctx.stroke();
|
|
971
|
+
}
|
|
972
|
+
drawCurvedPath(ctx, start, end) {
|
|
973
|
+
const [x1, y1] = start;
|
|
974
|
+
const [x2, y2] = end;
|
|
975
|
+
const dx = Math.abs(x1 - x2) * 0.7;
|
|
976
|
+
ctx.moveTo(x1, y1);
|
|
977
|
+
ctx.bezierCurveTo(x1 + dx, y1, x2 - dx, y2, x2, y2);
|
|
978
|
+
}
|
|
979
|
+
onMouseUp(event) {
|
|
980
|
+
if (this.isLocked())
|
|
981
|
+
return;
|
|
982
|
+
this.endPan();
|
|
983
|
+
this.endNodeDrag();
|
|
984
|
+
}
|
|
985
|
+
onClick(event) {
|
|
986
|
+
const target = event.target;
|
|
987
|
+
if (this.draggingConnection()) {
|
|
988
|
+
if (!target.closest('.port')) {
|
|
989
|
+
this.cancelPortDrag();
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
else if (this.isLocked() && !target.closest('.midpoint-div')) {
|
|
993
|
+
this.closeMidpointDiv();
|
|
994
|
+
}
|
|
995
|
+
else {
|
|
996
|
+
this.#handleConnectionClick(event);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
onEscape(event) {
|
|
1000
|
+
if (this.draggingConnection()) {
|
|
1001
|
+
this.cancelPortDrag();
|
|
1002
|
+
}
|
|
1003
|
+
else if (this.isLocked()) {
|
|
1004
|
+
this.closeMidpointDiv();
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
onMouseMove(event) {
|
|
1008
|
+
if (this.isLocked())
|
|
1009
|
+
return;
|
|
1010
|
+
if (this.#isNodeDragging()) {
|
|
1011
|
+
this.nodeDrag(event);
|
|
1012
|
+
}
|
|
1013
|
+
else if (this.draggingConnection()) {
|
|
1014
|
+
this.updatePathOnMove(event);
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
this.pan(event);
|
|
1018
|
+
if (this.isHoveringNode()) {
|
|
1019
|
+
this.highlightedConnection.set(null);
|
|
1020
|
+
}
|
|
1021
|
+
else {
|
|
1022
|
+
this.#checkConnectionHover(event);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
onTouchMove(event) {
|
|
1027
|
+
if (this.isLocked())
|
|
1028
|
+
return;
|
|
1029
|
+
if (this.#isNodeDragging()) {
|
|
1030
|
+
this.nodeDrag(event.touches[0]);
|
|
1031
|
+
}
|
|
1032
|
+
else if (this.draggingConnection()) {
|
|
1033
|
+
this.updatePathOnMove(event.touches[0]);
|
|
1034
|
+
}
|
|
1035
|
+
else {
|
|
1036
|
+
this.handleTouchMove(event);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
startNodeDrag(event, nodeId) {
|
|
1040
|
+
event.stopPropagation();
|
|
1041
|
+
event.preventDefault();
|
|
1042
|
+
this.#isNodeDragging.set(true);
|
|
1043
|
+
this.#draggedNodeId.set(nodeId);
|
|
1044
|
+
const clientX = event instanceof MouseEvent ? event.clientX : event.touches[0].clientX;
|
|
1045
|
+
const clientY = event instanceof MouseEvent ? event.clientY : event.touches[0].clientY;
|
|
1046
|
+
const blueprintRect = this.#selfRef.nativeElement.getBoundingClientRect();
|
|
1047
|
+
const worldX = (clientX - blueprintRect.left - this.panX()) / this.zoomLevel();
|
|
1048
|
+
const worldY = (clientY - blueprintRect.top - this.panY()) / this.zoomLevel();
|
|
1049
|
+
const draggedNode = this.nodes().find((n) => n.id === nodeId);
|
|
1050
|
+
if (draggedNode) {
|
|
1051
|
+
this.#dragOffset.set([worldX - draggedNode.coordinates[0], worldY - draggedNode.coordinates[1]]);
|
|
1052
|
+
}
|
|
1053
|
+
this.#lastMouseX.set(clientX);
|
|
1054
|
+
this.#lastMouseY.set(clientY);
|
|
1055
|
+
}
|
|
1056
|
+
endNodeDrag() {
|
|
1057
|
+
const draggedId = this.#draggedNodeId();
|
|
1058
|
+
const finalCoords = this.#draggingNodeCoordinates();
|
|
1059
|
+
if (draggedId && finalCoords) {
|
|
1060
|
+
this.nodes.update((nodes) => {
|
|
1061
|
+
const node = nodes.find((n) => n.id === draggedId);
|
|
1062
|
+
if (node) {
|
|
1063
|
+
node.coordinates = finalCoords;
|
|
1064
|
+
}
|
|
1065
|
+
return [...nodes];
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
this.#isNodeDragging.set(false);
|
|
1069
|
+
this.#draggedNodeId.set(null);
|
|
1070
|
+
this.#dragOffset.set(null);
|
|
1071
|
+
this.#draggingNodeCoordinates.set(null);
|
|
1072
|
+
}
|
|
1073
|
+
nodeDrag(event) {
|
|
1074
|
+
const draggedId = this.#draggedNodeId();
|
|
1075
|
+
if (!this.#isNodeDragging() || !draggedId)
|
|
1076
|
+
return;
|
|
1077
|
+
const { clientX, clientY } = event;
|
|
1078
|
+
const blueprintRect = this.#selfRef.nativeElement.getBoundingClientRect();
|
|
1079
|
+
const worldX = (clientX - blueprintRect.left - this.panX()) / this.zoomLevel();
|
|
1080
|
+
const worldY = (clientY - blueprintRect.top - this.panY()) / this.zoomLevel();
|
|
1081
|
+
let newX = worldX - this.#dragOffset()[0];
|
|
1082
|
+
let newY = worldY - this.#dragOffset()[1];
|
|
1083
|
+
if (this.snapToGrid() || (event instanceof MouseEvent && event.shiftKey)) {
|
|
1084
|
+
const grid = this.gridSnapSize();
|
|
1085
|
+
newX = Math.round(newX / grid) * grid;
|
|
1086
|
+
newY = Math.round(newY / grid) * grid;
|
|
1087
|
+
}
|
|
1088
|
+
this.#draggingNodeCoordinates.set([newX, newY]);
|
|
1089
|
+
this.#lastMouseX.set(clientX);
|
|
1090
|
+
this.#lastMouseY.set(clientY);
|
|
1091
|
+
}
|
|
1092
|
+
startPortDrag(event, nodeId, portId) {
|
|
1093
|
+
event.stopPropagation();
|
|
1094
|
+
if (this.draggingConnection())
|
|
1095
|
+
this.cancelPortDrag();
|
|
1096
|
+
const node = this.nodes().find((n) => n.id === nodeId);
|
|
1097
|
+
if (node) {
|
|
1098
|
+
this.draggingConnection.set({
|
|
1099
|
+
fromNode: nodeId,
|
|
1100
|
+
fromPort: portId,
|
|
1101
|
+
x2: node.coordinates[0],
|
|
1102
|
+
y2: node.coordinates[1],
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
this.updatePathOnMove(event);
|
|
1106
|
+
}
|
|
1107
|
+
endPortDrag(_, toNodeId, toPortId) {
|
|
1108
|
+
if (!this.draggingConnection())
|
|
1109
|
+
return;
|
|
1110
|
+
const from = this.draggingConnection();
|
|
1111
|
+
if (from.fromNode === toNodeId) {
|
|
1112
|
+
this.cancelPortDrag();
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
const newConnection = {
|
|
1116
|
+
fromNode: from.fromNode,
|
|
1117
|
+
fromPort: from.fromPort,
|
|
1118
|
+
toNode: toNodeId,
|
|
1119
|
+
toPort: toPortId,
|
|
1120
|
+
};
|
|
1121
|
+
this.nodes.update((nodes) => {
|
|
1122
|
+
const fromNode = nodes.find((n) => n.id === newConnection.fromNode);
|
|
1123
|
+
const toNode = nodes.find((n) => n.id === newConnection.toNode);
|
|
1124
|
+
if (fromNode && toNode) {
|
|
1125
|
+
const isDuplicateFrom = fromNode.connections.some((c) => c.fromNode === newConnection.fromNode &&
|
|
1126
|
+
c.fromPort === newConnection.fromPort &&
|
|
1127
|
+
c.toNode === newConnection.toNode &&
|
|
1128
|
+
c.toPort === newConnection.toPort);
|
|
1129
|
+
if (!isDuplicateFrom) {
|
|
1130
|
+
fromNode.connections = [...fromNode.connections, newConnection];
|
|
1131
|
+
}
|
|
1132
|
+
const isDuplicateTo = toNode.connections.some((c) => c.fromNode === newConnection.fromNode &&
|
|
1133
|
+
c.fromPort === newConnection.fromPort &&
|
|
1134
|
+
c.toNode === newConnection.toNode &&
|
|
1135
|
+
c.toPort === newConnection.toPort);
|
|
1136
|
+
if (!isDuplicateTo) {
|
|
1137
|
+
toNode.connections = [...toNode.connections, newConnection];
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return [...nodes];
|
|
1141
|
+
});
|
|
1142
|
+
this.cancelPortDrag();
|
|
1143
|
+
}
|
|
1144
|
+
cancelPortDrag() {
|
|
1145
|
+
this.draggingConnection.set(null);
|
|
1146
|
+
}
|
|
1147
|
+
updatePathOnMove(event) {
|
|
1148
|
+
if (!this.draggingConnection())
|
|
1149
|
+
return;
|
|
1150
|
+
const blueprintRect = this.#selfRef.nativeElement.getBoundingClientRect();
|
|
1151
|
+
const x2 = (event.clientX - blueprintRect.left - this.panX()) / this.zoomLevel();
|
|
1152
|
+
const y2 = (event.clientY - blueprintRect.top - this.panY()) / this.zoomLevel();
|
|
1153
|
+
this.draggingConnection.update((conn) => (conn ? { ...conn, x2, y2 } : conn));
|
|
1154
|
+
}
|
|
1155
|
+
getNodePortPosition(nodeId, portId) {
|
|
1156
|
+
const node = this.nodes().find((n) => n.id === nodeId);
|
|
1157
|
+
if (!node)
|
|
1158
|
+
return [0, 0];
|
|
1159
|
+
const portEl = this.#selfRef.nativeElement.querySelector(`[data-node-id="${nodeId}"][data-port-id="${portId}"]`);
|
|
1160
|
+
if (!portEl)
|
|
1161
|
+
return node.coordinates;
|
|
1162
|
+
const nodeWrapper = this.#selfRef.nativeElement.querySelector('.nodes-wrapper');
|
|
1163
|
+
const wrapperRect = nodeWrapper.getBoundingClientRect();
|
|
1164
|
+
const portRect = portEl.getBoundingClientRect();
|
|
1165
|
+
const portCenterX = portRect.left + portRect.width / 2;
|
|
1166
|
+
const portCenterY = portRect.top + portRect.height / 2;
|
|
1167
|
+
const worldX = (portCenterX - wrapperRect.left) / this.zoomLevel();
|
|
1168
|
+
const worldY = (portCenterY - wrapperRect.top) / this.zoomLevel();
|
|
1169
|
+
return [worldX, worldY];
|
|
1170
|
+
}
|
|
1171
|
+
startPan(event) {
|
|
1172
|
+
if (this.isLocked())
|
|
1173
|
+
return;
|
|
1174
|
+
if (event.target instanceof HTMLElement && event.target.closest('.node'))
|
|
1175
|
+
return;
|
|
1176
|
+
event.preventDefault();
|
|
1177
|
+
this.#isDragging.set(true);
|
|
1178
|
+
this.#lastMouseX.set(event.clientX);
|
|
1179
|
+
this.#lastMouseY.set(event.clientY);
|
|
1180
|
+
}
|
|
1181
|
+
endPan() {
|
|
1182
|
+
this.#isDragging.set(false);
|
|
1183
|
+
}
|
|
1184
|
+
pan(event) {
|
|
1185
|
+
if (!this.#isDragging() || this.isLocked())
|
|
1186
|
+
return;
|
|
1187
|
+
const dx = event.clientX - this.#lastMouseX();
|
|
1188
|
+
const dy = event.clientY - this.#lastMouseY();
|
|
1189
|
+
this.panX.update((x) => x + dx);
|
|
1190
|
+
this.panY.update((y) => y + dy);
|
|
1191
|
+
this.#lastMouseX.set(event.clientX);
|
|
1192
|
+
this.#lastMouseY.set(event.clientY);
|
|
1193
|
+
}
|
|
1194
|
+
zoom(event) {
|
|
1195
|
+
event.preventDefault();
|
|
1196
|
+
const oldZoom = this.zoomLevel();
|
|
1197
|
+
const newZoom = this.#clamp(oldZoom * (1 - event.deltaY * this.#ZOOM_SPEED), this.#MIN_ZOOM, this.#MAX_ZOOM);
|
|
1198
|
+
const panRatio = newZoom / oldZoom;
|
|
1199
|
+
const newPanX = event.clientX - (event.clientX - this.panX()) * panRatio;
|
|
1200
|
+
const newPanY = event.clientY - (event.clientY - this.panY()) * panRatio;
|
|
1201
|
+
this.zoomLevel.set(newZoom);
|
|
1202
|
+
this.panX.set(newPanX);
|
|
1203
|
+
this.panY.set(newPanY);
|
|
1204
|
+
}
|
|
1205
|
+
handleTouchStart(event) {
|
|
1206
|
+
if (event.target instanceof HTMLElement && event.target.closest('.node'))
|
|
1207
|
+
return;
|
|
1208
|
+
event.preventDefault();
|
|
1209
|
+
if (event.touches.length === 1) {
|
|
1210
|
+
this.#isDragging.set(true);
|
|
1211
|
+
this.#lastMouseX.set(event.touches[0].clientX);
|
|
1212
|
+
this.#lastMouseY.set(event.touches[0].clientY);
|
|
1213
|
+
}
|
|
1214
|
+
else if (event.touches.length === 2) {
|
|
1215
|
+
this.#isDragging.set(false);
|
|
1216
|
+
this.#initialPinchDistance.set(this.#getDistance(event.touches[0], event.touches[1]));
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
handleTouchMove(event) {
|
|
1220
|
+
if (event.touches.length === 1 && this.#isDragging()) {
|
|
1221
|
+
const dx = event.touches[0].clientX - this.#lastMouseX();
|
|
1222
|
+
const dy = event.touches[0].clientY - this.#lastMouseY();
|
|
1223
|
+
this.panX.update((x) => x + dx);
|
|
1224
|
+
this.panY.update((y) => y + dy);
|
|
1225
|
+
this.#lastMouseX.set(event.touches[0].clientX);
|
|
1226
|
+
this.#lastMouseY.set(event.touches[0].clientY);
|
|
1227
|
+
}
|
|
1228
|
+
else if (event.touches.length === 2) {
|
|
1229
|
+
const newPinchDistance = this.#getDistance(event.touches[0], event.touches[1]);
|
|
1230
|
+
const pinchRatio = newPinchDistance / this.#initialPinchDistance();
|
|
1231
|
+
const oldZoom = this.zoomLevel();
|
|
1232
|
+
const newZoom = this.#clamp(oldZoom * pinchRatio, this.#MIN_ZOOM, this.#MAX_ZOOM);
|
|
1233
|
+
const pinchCenterX = (event.touches[0].clientX + event.touches[1].clientX) / 2;
|
|
1234
|
+
const pinchCenterY = (event.touches[0].clientY + event.touches[1].clientY) / 2;
|
|
1235
|
+
const panRatio = newZoom / oldZoom;
|
|
1236
|
+
const newPanX = pinchCenterX - (pinchCenterX - this.panX()) * panRatio;
|
|
1237
|
+
const newPanY = pinchCenterY - (pinchCenterY - this.panY()) * panRatio;
|
|
1238
|
+
this.zoomLevel.set(newZoom);
|
|
1239
|
+
this.panX.set(newPanX);
|
|
1240
|
+
this.panY.set(newPanY);
|
|
1241
|
+
this.#initialPinchDistance.set(newPinchDistance);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
handleTouchEnd() {
|
|
1245
|
+
this.#isDragging.set(false);
|
|
1246
|
+
}
|
|
1247
|
+
closeMidpointDiv() {
|
|
1248
|
+
this.showMidpointDiv.set(false);
|
|
1249
|
+
this.midpointDivPosition.set(null);
|
|
1250
|
+
this.isLocked.set(false);
|
|
1251
|
+
}
|
|
1252
|
+
getDisplayCoordinates(node) {
|
|
1253
|
+
const draggedId = this.#draggedNodeId();
|
|
1254
|
+
const draggingCoords = this.#draggingNodeCoordinates();
|
|
1255
|
+
if (draggedId === node.id && draggingCoords) {
|
|
1256
|
+
return `translate(${draggingCoords[0]}px, ${draggingCoords[1]}px)`;
|
|
1257
|
+
}
|
|
1258
|
+
return `translate(${node.coordinates[0]}px, ${node.coordinates[1]}px)`;
|
|
1259
|
+
}
|
|
1260
|
+
removeConnection() {
|
|
1261
|
+
const conn = this.highlightedConnection();
|
|
1262
|
+
if (!conn)
|
|
1263
|
+
return;
|
|
1264
|
+
this.#connections.update((conns) => conns.filter((c) => !(c.fromNode === conn.fromNode &&
|
|
1265
|
+
c.fromPort === conn.fromPort &&
|
|
1266
|
+
c.toNode === conn.toNode &&
|
|
1267
|
+
c.toPort === conn.toPort)));
|
|
1268
|
+
this.nodes.update((nodes) => nodes.map((n) => ({
|
|
1269
|
+
...n,
|
|
1270
|
+
connections: n.connections.filter((c) => !(c.fromNode === conn.fromNode &&
|
|
1271
|
+
c.fromPort === conn.fromPort &&
|
|
1272
|
+
c.toNode === conn.toNode &&
|
|
1273
|
+
c.toPort === conn.toPort)),
|
|
1274
|
+
})));
|
|
1275
|
+
this.highlightedConnection.set(null);
|
|
1276
|
+
this.closeMidpointDiv();
|
|
1277
|
+
}
|
|
1278
|
+
#removeDuplicatesAndReinitiate() {
|
|
1279
|
+
const nodes = this.nodes();
|
|
1280
|
+
const uniqueNodesMap = new Map();
|
|
1281
|
+
const uniqueNodes = [];
|
|
1282
|
+
for (const node of nodes) {
|
|
1283
|
+
if (!uniqueNodesMap.has(node.id)) {
|
|
1284
|
+
uniqueNodesMap.set(node.id, node);
|
|
1285
|
+
uniqueNodes.push(node);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
const nodesWithUniquePorts = uniqueNodes.map((node) => {
|
|
1289
|
+
const uniqueInputs = new Map();
|
|
1290
|
+
const uniqueOutputs = new Map();
|
|
1291
|
+
const uniqueNodeInputs = node.inputs.filter((port) => {
|
|
1292
|
+
if (!uniqueInputs.has(port.id)) {
|
|
1293
|
+
uniqueInputs.set(port.id, port);
|
|
1294
|
+
return true;
|
|
1295
|
+
}
|
|
1296
|
+
return false;
|
|
1297
|
+
});
|
|
1298
|
+
const uniqueNodeOutputs = node.outputs.filter((port) => {
|
|
1299
|
+
if (!uniqueOutputs.has(port.id)) {
|
|
1300
|
+
uniqueOutputs.set(port.id, port);
|
|
1301
|
+
return true;
|
|
1302
|
+
}
|
|
1303
|
+
return false;
|
|
1304
|
+
});
|
|
1305
|
+
return {
|
|
1306
|
+
...node,
|
|
1307
|
+
inputs: uniqueNodeInputs,
|
|
1308
|
+
outputs: uniqueNodeOutputs,
|
|
1309
|
+
};
|
|
1310
|
+
});
|
|
1311
|
+
const finalNodes = nodesWithUniquePorts.map((node) => {
|
|
1312
|
+
const validConnections = node.connections.filter((conn) => {
|
|
1313
|
+
const fromNodeExists = nodesWithUniquePorts.some((n) => n.id === conn.fromNode);
|
|
1314
|
+
const toNodeExists = nodesWithUniquePorts.some((n) => n.id === conn.toNode);
|
|
1315
|
+
const fromPortExists = nodesWithUniquePorts
|
|
1316
|
+
.find((n) => n.id === conn.fromNode)
|
|
1317
|
+
?.outputs.some((p) => p.id === conn.fromPort);
|
|
1318
|
+
const toPortExists = nodesWithUniquePorts
|
|
1319
|
+
.find((n) => n.id === conn.toNode)
|
|
1320
|
+
?.inputs.some((p) => p.id === conn.toPort);
|
|
1321
|
+
return fromNodeExists && toNodeExists && fromPortExists && toPortExists;
|
|
1322
|
+
});
|
|
1323
|
+
return { ...node, connections: validConnections };
|
|
1324
|
+
});
|
|
1325
|
+
this.nodes.set(finalNodes);
|
|
1326
|
+
}
|
|
1327
|
+
#handleConnectionClick(event) {
|
|
1328
|
+
if (this.highlightedConnection()) {
|
|
1329
|
+
const conn = this.highlightedConnection();
|
|
1330
|
+
const startPos = this.getNodePortPosition(conn.fromNode, conn.fromPort);
|
|
1331
|
+
const endPos = this.getNodePortPosition(conn.toNode, conn.toPort);
|
|
1332
|
+
const midpoint = this.#getBezierMidpoint(startPos, endPos);
|
|
1333
|
+
this.midpointDivPosition.set({ x: midpoint[0], y: midpoint[1] });
|
|
1334
|
+
this.showMidpointDiv.set(true);
|
|
1335
|
+
this.isLocked.set(true);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
#getBezierMidpoint(start, end) {
|
|
1339
|
+
const t = 0.5;
|
|
1340
|
+
const [x1, y1] = start;
|
|
1341
|
+
const [x2, y2] = end;
|
|
1342
|
+
const dx = Math.abs(x1 - x2) * 0.7;
|
|
1343
|
+
const cp1x = x1 + dx;
|
|
1344
|
+
const cp1y = y1;
|
|
1345
|
+
const cp2x = x2 - dx;
|
|
1346
|
+
const cp2y = y2;
|
|
1347
|
+
const mx = Math.pow(1 - t, 3) * x1 +
|
|
1348
|
+
3 * Math.pow(1 - t, 2) * t * cp1x +
|
|
1349
|
+
3 * (1 - t) * Math.pow(t, 2) * cp2x +
|
|
1350
|
+
Math.pow(t, 3) * x2;
|
|
1351
|
+
const my = Math.pow(1 - t, 3) * y1 +
|
|
1352
|
+
3 * Math.pow(1 - t, 2) * t * cp1y +
|
|
1353
|
+
3 * (1 - t) * Math.pow(t, 2) * cp2y +
|
|
1354
|
+
Math.pow(t, 3) * y2;
|
|
1355
|
+
return [mx, my];
|
|
1356
|
+
}
|
|
1357
|
+
#checkConnectionHover(event) {
|
|
1358
|
+
const blueprintRect = this.#selfRef.nativeElement.getBoundingClientRect();
|
|
1359
|
+
const worldX = (event.clientX - blueprintRect.left - this.panX()) / this.zoomLevel();
|
|
1360
|
+
const worldY = (event.clientY - blueprintRect.top - this.panY()) / this.zoomLevel();
|
|
1361
|
+
const cursorPoint = [worldX, worldY];
|
|
1362
|
+
let hoveredConn = null;
|
|
1363
|
+
const tolerance = 10 / this.zoomLevel();
|
|
1364
|
+
for (const conn of this.#connections()) {
|
|
1365
|
+
const startPos = this.getNodePortPosition(conn.fromNode, conn.fromPort);
|
|
1366
|
+
const endPos = this.getNodePortPosition(conn.toNode, conn.toPort);
|
|
1367
|
+
if (this.#isPointNearBezierCurve(cursorPoint, startPos, endPos, tolerance)) {
|
|
1368
|
+
hoveredConn = conn;
|
|
1369
|
+
break;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
this.highlightedConnection.set(hoveredConn);
|
|
1373
|
+
}
|
|
1374
|
+
#isPointNearBezierCurve(point, start, end, tolerance = 10) {
|
|
1375
|
+
const [x1, y1] = start;
|
|
1376
|
+
const [x2, y2] = end;
|
|
1377
|
+
const dx = Math.abs(x1 - x2) * 0.7;
|
|
1378
|
+
const cp1x = x1 + dx;
|
|
1379
|
+
const cp1y = y1;
|
|
1380
|
+
const cp2x = x2 - dx;
|
|
1381
|
+
const cp2y = y2;
|
|
1382
|
+
const steps = 20;
|
|
1383
|
+
for (let i = 0; i <= steps; i++) {
|
|
1384
|
+
const t = i / steps;
|
|
1385
|
+
const x = Math.pow(1 - t, 3) * x1 +
|
|
1386
|
+
3 * Math.pow(1 - t, 2) * t * cp1x +
|
|
1387
|
+
3 * (1 - t) * Math.pow(t, 2) * cp2x +
|
|
1388
|
+
Math.pow(t, 3) * x2;
|
|
1389
|
+
const y = Math.pow(1 - t, 3) * y1 +
|
|
1390
|
+
3 * Math.pow(1 - t, 2) * t * cp1y +
|
|
1391
|
+
3 * (1 - t) * Math.pow(t, 2) * cp2y +
|
|
1392
|
+
Math.pow(t, 3) * y2;
|
|
1393
|
+
const distance = Math.sqrt(Math.pow(point[0] - x, 2) + Math.pow(point[1] - y, 2));
|
|
1394
|
+
if (distance < tolerance) {
|
|
1395
|
+
return true;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return false;
|
|
1399
|
+
}
|
|
1400
|
+
getNewNodeCoordinates(panToCoordinates = false) {
|
|
1401
|
+
let lowestY = 0;
|
|
1402
|
+
if (this.nodes().length > 0) {
|
|
1403
|
+
lowestY = this.nodes().reduce((max, node) => Math.max(max, node.coordinates[1]), 0);
|
|
1404
|
+
}
|
|
1405
|
+
const newCoordinates = [20, lowestY + 200];
|
|
1406
|
+
if (panToCoordinates) {
|
|
1407
|
+
this.#panToCoordinates(newCoordinates);
|
|
1408
|
+
}
|
|
1409
|
+
return newCoordinates;
|
|
1410
|
+
}
|
|
1411
|
+
#panToCoordinates(coords) {
|
|
1412
|
+
const [x, y] = coords;
|
|
1413
|
+
const rect = this.#selfRef.nativeElement.getBoundingClientRect();
|
|
1414
|
+
this.panX.set(rect.width / 2 - x * this.zoomLevel());
|
|
1415
|
+
this.panY.set(rect.height / 2 - y * this.zoomLevel());
|
|
1416
|
+
}
|
|
1417
|
+
#getDistance(touch1, touch2) {
|
|
1418
|
+
const dx = touch1.clientX - touch2.clientX;
|
|
1419
|
+
const dy = touch1.clientY - touch2.clientY;
|
|
1420
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
1421
|
+
}
|
|
1422
|
+
#clamp(value, min, max) {
|
|
1423
|
+
return Math.max(min, Math.min(max, value));
|
|
1424
|
+
}
|
|
1425
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ShipBlueprintComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1426
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.0", type: ShipBlueprintComponent, isStandalone: true, selector: "sh-blueprint", inputs: { forceUnique: { classPropertyName: "forceUnique", publicName: "forceUnique", isSignal: true, isRequired: false, transformFunction: null }, autoLayout: { classPropertyName: "autoLayout", publicName: "autoLayout", isSignal: true, isRequired: false, transformFunction: null }, gridSize: { classPropertyName: "gridSize", publicName: "gridSize", isSignal: true, isRequired: false, transformFunction: null }, snapToGrid: { classPropertyName: "snapToGrid", publicName: "snapToGrid", isSignal: true, isRequired: false, transformFunction: null }, gridColor: { classPropertyName: "gridColor", publicName: "gridColor", isSignal: true, isRequired: false, transformFunction: null }, nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodes: "nodesChange" }, host: { listeners: { "document:mouseup": "onMouseUp($event)", "document:click": "onClick($event)", "document:keydown.escape": "onEscape($event)", "document:mousemove": "onMouseMove($event)", "document:touchmove": "onTouchMove($event)" } }, viewQueries: [{ propertyName: "canvasRef", first: true, predicate: ["blueprintCanvas"], descendants: true, static: true }], ngImport: i0, template: `
|
|
1427
|
+
<div
|
|
1428
|
+
class="canvas-container"
|
|
1429
|
+
[class.locked]="isLocked()"
|
|
1430
|
+
[class.hovering-connection]="highlightedConnection()"
|
|
1431
|
+
(mousedown)="startPan($event)"
|
|
1432
|
+
(mousemove)="pan($event)"
|
|
1433
|
+
(wheel)="zoom($event)"
|
|
1434
|
+
(touchstart)="handleTouchStart($event)"
|
|
1435
|
+
(touchmove)="handleTouchMove($event)"
|
|
1436
|
+
(touchend)="handleTouchEnd()">
|
|
1437
|
+
<div class="action-panel">
|
|
1438
|
+
<button shButton (click)="applyAutolayout()" class="small">
|
|
1439
|
+
Autolayout
|
|
1440
|
+
<sh-icon>tree-structure</sh-icon>
|
|
1441
|
+
</button>
|
|
1442
|
+
|
|
1443
|
+
<ng-content />
|
|
1444
|
+
</div>
|
|
1445
|
+
|
|
1446
|
+
<canvas #blueprintCanvas></canvas>
|
|
1447
|
+
|
|
1448
|
+
@if (validationErrors()) {
|
|
1449
|
+
<sh-card class="validation-errors type-c">
|
|
1450
|
+
Something went wrong you have
|
|
1451
|
+
<br />
|
|
1452
|
+
duplicate node IDs or port IDs
|
|
1453
|
+
<pre>{{ validationErrors() | json }}</pre>
|
|
1454
|
+
</sh-card>
|
|
1455
|
+
} @else {
|
|
1456
|
+
<div
|
|
1457
|
+
class="nodes-wrapper"
|
|
1458
|
+
[style.transform]="'translate(' + panX() + 'px, ' + panY() + 'px) scale(' + zoomLevel() + ')'"
|
|
1459
|
+
[style.transform-origin]="'0 0'">
|
|
1460
|
+
@for (node of nodes(); track node.id) {
|
|
1461
|
+
<sh-card
|
|
1462
|
+
class="node type-c"
|
|
1463
|
+
[style.transform]="getDisplayCoordinates(node)"
|
|
1464
|
+
(mouseenter)="isHoveringNode.set(true)"
|
|
1465
|
+
(mouseleave)="isHoveringNode.set(false)">
|
|
1466
|
+
<header (mousedown)="startNodeDrag($event, node.id)" (touchstart)="startNodeDrag($event, node.id)">
|
|
1467
|
+
<span class="node-title">{{ node.name ? node.name : node.id }}</span>
|
|
1468
|
+
<sh-icon>list</sh-icon>
|
|
1469
|
+
</header>
|
|
1470
|
+
<div class="ports">
|
|
1471
|
+
<div class="inputs">
|
|
1472
|
+
@for (inputPort of node.inputs; track inputPort.id) {
|
|
1473
|
+
<div class="port-wrap">
|
|
1474
|
+
<div
|
|
1475
|
+
class="port input-port"
|
|
1476
|
+
[attr.data-node-id]="node.id"
|
|
1477
|
+
[attr.data-port-id]="inputPort.id"
|
|
1478
|
+
(click)="endPortDrag($event, node.id, inputPort.id)"></div>
|
|
1479
|
+
<span class="port-name">{{ inputPort.name }}</span>
|
|
1480
|
+
</div>
|
|
1481
|
+
}
|
|
1482
|
+
</div>
|
|
1483
|
+
<div class="outputs">
|
|
1484
|
+
@for (outputPort of node.outputs; track outputPort.id) {
|
|
1485
|
+
<div class="port-wrap">
|
|
1486
|
+
<span class="port-name">{{ outputPort.name }}</span>
|
|
1487
|
+
<div
|
|
1488
|
+
class="port output-port"
|
|
1489
|
+
[attr.data-node-id]="node.id"
|
|
1490
|
+
[attr.data-port-id]="outputPort.id"
|
|
1491
|
+
(click)="startPortDrag($event, node.id, outputPort.id)"></div>
|
|
1492
|
+
</div>
|
|
1493
|
+
}
|
|
1494
|
+
</div>
|
|
1495
|
+
</div>
|
|
1496
|
+
</sh-card>
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
@if (showMidpointDiv()) {
|
|
1500
|
+
<div
|
|
1501
|
+
class="midpoint-div"
|
|
1502
|
+
(click)="removeConnection()"
|
|
1503
|
+
[style.left.px]="midpointDivPosition()?.x"
|
|
1504
|
+
[style.top.px]="midpointDivPosition()?.y">
|
|
1505
|
+
Remove connection
|
|
1506
|
+
<sh-icon>trash</sh-icon>
|
|
1507
|
+
</div>
|
|
1508
|
+
}
|
|
1509
|
+
</div>
|
|
1510
|
+
}
|
|
1511
|
+
</div>
|
|
1512
|
+
`, isInline: true, dependencies: [{ kind: "component", type: ShipCardComponent$1, selector: "sh-card" }, { kind: "component", type: ShipIconComponent$1, selector: "sh-icon" }, { kind: "component", type: ShipButtonComponent$1, selector: "[shButton]" }, { kind: "pipe", type: JsonPipe, name: "json" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1513
|
+
}
|
|
1514
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ShipBlueprintComponent, decorators: [{
|
|
1515
|
+
type: Component,
|
|
1516
|
+
args: [{
|
|
1517
|
+
selector: 'sh-blueprint',
|
|
1518
|
+
imports: [ShipCardComponent$1, ShipIconComponent$1, JsonPipe, ShipButtonComponent$1],
|
|
1519
|
+
template: `
|
|
1520
|
+
<div
|
|
1521
|
+
class="canvas-container"
|
|
1522
|
+
[class.locked]="isLocked()"
|
|
1523
|
+
[class.hovering-connection]="highlightedConnection()"
|
|
1524
|
+
(mousedown)="startPan($event)"
|
|
1525
|
+
(mousemove)="pan($event)"
|
|
1526
|
+
(wheel)="zoom($event)"
|
|
1527
|
+
(touchstart)="handleTouchStart($event)"
|
|
1528
|
+
(touchmove)="handleTouchMove($event)"
|
|
1529
|
+
(touchend)="handleTouchEnd()">
|
|
1530
|
+
<div class="action-panel">
|
|
1531
|
+
<button shButton (click)="applyAutolayout()" class="small">
|
|
1532
|
+
Autolayout
|
|
1533
|
+
<sh-icon>tree-structure</sh-icon>
|
|
1534
|
+
</button>
|
|
1535
|
+
|
|
1536
|
+
<ng-content />
|
|
1537
|
+
</div>
|
|
1538
|
+
|
|
1539
|
+
<canvas #blueprintCanvas></canvas>
|
|
1540
|
+
|
|
1541
|
+
@if (validationErrors()) {
|
|
1542
|
+
<sh-card class="validation-errors type-c">
|
|
1543
|
+
Something went wrong you have
|
|
1544
|
+
<br />
|
|
1545
|
+
duplicate node IDs or port IDs
|
|
1546
|
+
<pre>{{ validationErrors() | json }}</pre>
|
|
1547
|
+
</sh-card>
|
|
1548
|
+
} @else {
|
|
1549
|
+
<div
|
|
1550
|
+
class="nodes-wrapper"
|
|
1551
|
+
[style.transform]="'translate(' + panX() + 'px, ' + panY() + 'px) scale(' + zoomLevel() + ')'"
|
|
1552
|
+
[style.transform-origin]="'0 0'">
|
|
1553
|
+
@for (node of nodes(); track node.id) {
|
|
1554
|
+
<sh-card
|
|
1555
|
+
class="node type-c"
|
|
1556
|
+
[style.transform]="getDisplayCoordinates(node)"
|
|
1557
|
+
(mouseenter)="isHoveringNode.set(true)"
|
|
1558
|
+
(mouseleave)="isHoveringNode.set(false)">
|
|
1559
|
+
<header (mousedown)="startNodeDrag($event, node.id)" (touchstart)="startNodeDrag($event, node.id)">
|
|
1560
|
+
<span class="node-title">{{ node.name ? node.name : node.id }}</span>
|
|
1561
|
+
<sh-icon>list</sh-icon>
|
|
1562
|
+
</header>
|
|
1563
|
+
<div class="ports">
|
|
1564
|
+
<div class="inputs">
|
|
1565
|
+
@for (inputPort of node.inputs; track inputPort.id) {
|
|
1566
|
+
<div class="port-wrap">
|
|
1567
|
+
<div
|
|
1568
|
+
class="port input-port"
|
|
1569
|
+
[attr.data-node-id]="node.id"
|
|
1570
|
+
[attr.data-port-id]="inputPort.id"
|
|
1571
|
+
(click)="endPortDrag($event, node.id, inputPort.id)"></div>
|
|
1572
|
+
<span class="port-name">{{ inputPort.name }}</span>
|
|
1573
|
+
</div>
|
|
1574
|
+
}
|
|
1575
|
+
</div>
|
|
1576
|
+
<div class="outputs">
|
|
1577
|
+
@for (outputPort of node.outputs; track outputPort.id) {
|
|
1578
|
+
<div class="port-wrap">
|
|
1579
|
+
<span class="port-name">{{ outputPort.name }}</span>
|
|
1580
|
+
<div
|
|
1581
|
+
class="port output-port"
|
|
1582
|
+
[attr.data-node-id]="node.id"
|
|
1583
|
+
[attr.data-port-id]="outputPort.id"
|
|
1584
|
+
(click)="startPortDrag($event, node.id, outputPort.id)"></div>
|
|
1585
|
+
</div>
|
|
1586
|
+
}
|
|
1587
|
+
</div>
|
|
1588
|
+
</div>
|
|
1589
|
+
</sh-card>
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
@if (showMidpointDiv()) {
|
|
1593
|
+
<div
|
|
1594
|
+
class="midpoint-div"
|
|
1595
|
+
(click)="removeConnection()"
|
|
1596
|
+
[style.left.px]="midpointDivPosition()?.x"
|
|
1597
|
+
[style.top.px]="midpointDivPosition()?.y">
|
|
1598
|
+
Remove connection
|
|
1599
|
+
<sh-icon>trash</sh-icon>
|
|
1600
|
+
</div>
|
|
1601
|
+
}
|
|
1602
|
+
</div>
|
|
1603
|
+
}
|
|
1604
|
+
</div>
|
|
1605
|
+
`,
|
|
1606
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1607
|
+
}]
|
|
1608
|
+
}], ctorParameters: () => [], propDecorators: { canvasRef: [{
|
|
1609
|
+
type: ViewChild,
|
|
1610
|
+
args: ['blueprintCanvas', { static: true }]
|
|
1611
|
+
}], onMouseUp: [{
|
|
1612
|
+
type: HostListener,
|
|
1613
|
+
args: ['document:mouseup', ['$event']]
|
|
1614
|
+
}], onClick: [{
|
|
1615
|
+
type: HostListener,
|
|
1616
|
+
args: ['document:click', ['$event']]
|
|
1617
|
+
}], onEscape: [{
|
|
1618
|
+
type: HostListener,
|
|
1619
|
+
args: ['document:keydown.escape', ['$event']]
|
|
1620
|
+
}], onMouseMove: [{
|
|
1621
|
+
type: HostListener,
|
|
1622
|
+
args: ['document:mousemove', ['$event']]
|
|
1623
|
+
}], onTouchMove: [{
|
|
1624
|
+
type: HostListener,
|
|
1625
|
+
args: ['document:touchmove', ['$event']]
|
|
1626
|
+
}] } });
|
|
1627
|
+
|
|
562
1628
|
class ShipButtonGroupComponent {
|
|
563
1629
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ShipButtonGroupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
564
1630
|
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.0", type: ShipButtonGroupComponent, isStandalone: true, selector: "sh-button-group", ngImport: i0, template: `
|
|
@@ -5696,5 +6762,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImpor
|
|
|
5696
6762
|
* Generated bundle index. Do not edit.
|
|
5697
6763
|
*/
|
|
5698
6764
|
|
|
5699
|
-
export { GridSortableDirective, SHIP_CONFIG, ShipAlertComponent, ShipAlertContainerComponent, ShipAlertModule, ShipAlertService, ShipButtonComponent, ShipButtonGroupComponent, ShipCardComponent, ShipCheckboxComponent, ShipChipComponent, ShipColorPickerComponent, ShipDatepickerComponent, ShipDatepickerInputComponent, ShipDaterangeInputComponent, ShipDialogComponent, ShipDialogService, ShipDividerComponent, ShipEventCardComponent, ShipFileDragDropDirective, ShipFileUploadComponent, ShipFormFieldComponent, ShipIconComponent, ShipListComponent, ShipMenuComponent, ShipPopoverComponent, ShipPreventWheelDirective, ShipProgressBarComponent, ShipRadioComponent, ShipRangeSliderComponent, ShipResizeDirective, ShipSelectComponent, ShipSidenavComponent, ShipSortDirective, ShipSortableComponent, ShipSortableDirective, ShipSpinnerComponent, ShipStepperComponent, ShipStickyColumnsDirective, ShipTableComponent, ShipTabsComponent, ShipToggleCardComponent, ShipToggleComponent, ShipTooltipComponent, ShipTooltipDirective, ShipTooltipWrapper, ShipVirtualScrollComponent, moveIndex, watchHostClass };
|
|
6765
|
+
export { GridSortableDirective, SHIP_CONFIG, ShipAlertComponent, ShipAlertContainerComponent, ShipAlertModule, ShipAlertService, ShipBlueprintComponent, ShipButtonComponent, ShipButtonGroupComponent, ShipCardComponent, ShipCheckboxComponent, ShipChipComponent, ShipColorPickerComponent, ShipDatepickerComponent, ShipDatepickerInputComponent, ShipDaterangeInputComponent, ShipDialogComponent, ShipDialogService, ShipDividerComponent, ShipEventCardComponent, ShipFileDragDropDirective, ShipFileUploadComponent, ShipFormFieldComponent, ShipIconComponent, ShipListComponent, ShipMenuComponent, ShipPopoverComponent, ShipPreventWheelDirective, ShipProgressBarComponent, ShipRadioComponent, ShipRangeSliderComponent, ShipResizeDirective, ShipSelectComponent, ShipSidenavComponent, ShipSortDirective, ShipSortableComponent, ShipSortableDirective, ShipSpinnerComponent, ShipStepperComponent, ShipStickyColumnsDirective, ShipTableComponent, ShipTabsComponent, ShipToggleCardComponent, ShipToggleComponent, ShipTooltipComponent, ShipTooltipDirective, ShipTooltipWrapper, ShipVirtualScrollComponent, TEST_NODES, moveIndex, watchHostClass };
|
|
5700
6766
|
//# sourceMappingURL=ship-ui-core.mjs.map
|