/
@Component({
standalone: true,
selector: 'editor',
imports: [
AsyncPipe,
CommonModule,
ReactiveFormsModule,
NgDrawFlowComponent,
TuiButton,
TuiInputNumber,
TuiLabel,
TuiTextfield,
TuiTextfieldComponent,
],
templateUrl: './editor.component.html',
styleUrls: ['./editor.component.less'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
dfPanZoomOptionsProvider({
leftPosition: 50,
}),
],
})
export default class EditorComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef);
public readonly drawFlowStore = inject(NgDrawFlowStoreService);
public readonly panZoomOptions = inject<DfPanZoomOptions>(DF_PAN_ZOOM_OPTIONS);
public readonly scaleControl = new FormControl<number>(1, {nonNullable: true});
public fullscreen$ = new BehaviorSubject<boolean>(false);
public counter = 0;
public form = new FormControl<DfDataModel>(this.data);
public data: DfDataModel = {
nodes: [
{
id: 'node-1',
data: {type: 'simpleNode', text: 'This base node'},
position: {x: 0, y: 0},
startNode: true,
},
{
id: 'node-2',
data: {type: 'simpleNode', text: 'This child node 1'},
position: {x: 250, y: 50},
},
{
id: 'node-3',
data: {type: 'simpleNode', text: 'This child node 2'},
position: {x: 250, y: -50},
},
{
id: 'node-4',
data: {type: 'simpleNode', text: 'This child node 3'},
position: {x: 500, y: 0},
endNode: true,
},
],
connections: [
{
source: {
nodeId: 'node-1',
connectorType: DfConnectionPoint.Output,
connectorId: 'node-1-output-1',
},
target: {
nodeId: 'node-2',
connectorType: DfConnectionPoint.Input,
connectorId: 'node-2-input-1',
},
},
{
source: {
nodeId: 'node-1',
connectorType: DfConnectionPoint.Output,
connectorId: 'node-1-output-1',
},
target: {
nodeId: 'node-3',
connectorType: DfConnectionPoint.Input,
connectorId: 'node-3-input-1',
},
},
{
source: {
nodeId: 'node-2',
connectorType: DfConnectionPoint.Output,
connectorId: 'node-2-output-1',
},
target: {
nodeId: 'node-4',
connectorType: DfConnectionPoint.Input,
connectorId: 'node-4-input-1',
},
},
{
source: {
nodeId: 'node-3',
connectorType: DfConnectionPoint.Output,
connectorId: 'node-3-output-1',
},
target: {
nodeId: 'node-4',
connectorType: DfConnectionPoint.Input,
connectorId: 'node-4-input-1',
},
},
],
};
public ngOnInit(): void {
this.scaleControl.valueChanges
.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef))
.subscribe((value: number) => {
if (!Number.isFinite(value)) {
return;
}
this.drawFlowStore.setScale(value);
});
}
public onScaleChange(zoomLevel: number): void {
this.scaleControl.setValue(zoomLevel / 100, {emitEvent: false});
}
public toggleFullscreen(): void {
this.fullscreen$.next(!this.fullscreen$.value);
this.drawFlowStore.resetPosition();
}
public addNodeToDrawFlow(): void {
const id = `new-node-${this.counter}`;
this.data.nodes = [
...this.data.nodes,
{
id,
data: {
type: 'simpleNode',
text: `created node ${this.counter + 1}`,
},
position: {x: 600, y: this.counter * 40},
},
];
this.form.setValue(this.data);
this.counter++;
}
}
.editor {
position: relative;
block-size: 31.25rem;
inline-size: 100%;
margin: 0 auto;
background: #fff;
&.fullscreen {
position: fixed;
z-index: 1;
inset-block-start: 0;
inset-inline-start: 0;
inline-size: 100%;
block-size: 100%;
background-color: #fff;
.add {
inset-block-start: 5rem;
}
.fullscreen-btn {
inset-block-start: 7.8rem;
}
.remove-node-btn {
inset-block-start: 10.6rem;
}
.selection-info {
inset-block-start: 5rem;
}
}
}
.add {
position: absolute;
inset-inline-end: 1.25rem;
inset-block-start: 1.25rem;
}
.fullscreen-btn {
position: absolute;
inset-inline-end: 1.25rem;
inset-block-start: 3.75rem;
}
.remove-node-btn {
position: absolute;
inset-inline-end: 1.25rem;
inset-block-start: 6.25rem;
}
.scale-controls {
position: absolute;
display: flex;
inset-block-end: 1.25rem;
inset-inline-end: 1.25rem;
align-items: center;
gap: 0.75rem;
}
.scale-value {
inline-size: 3.125rem;
text-align: center;
color: #333;
}
.scale-field {
background: #fff;
}
.selection-info {
position: absolute;
display: flex;
inset-block-start: 1.25rem;
inset-inline-start: 1.25rem;
flex-direction: column;
gap: 0.25rem;
padding: 0.75rem 1rem;
min-inline-size: 11rem;
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid #dcdcdc;
border-radius: 0.5rem;
box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.08);
}
.selection-title {
font-weight: 600;
color: #1f1f1f;
}
.selection-value {
font-size: 0.875rem;
color: #555;
}
<div
class="editor"
[ngClass]="{fullscreen: fullscreen$ | async}"
>
<ng-draw-flow
[formControl]="form"
(connectionCreated)="onConnectionCreated($event)"
(connectionDeleted)="onConnectionDeleted($event)"
(connectionSelected)="onConnectionSelected($event)"
(nodeDeleted)="onNodeDeleted($event)"
(nodeMoved)="onNodeMoved($event)"
(nodeSelected)="onNodeSelected($event)"
(scale)="onScaleChange($event)"
/>
<button
class="add"
size="s"
tuiButton
type="button"
(click)="addNodeToDrawFlow()"
>
Add node
</button>
<button
class="fullscreen-btn"
size="s"
tuiButton
type="button"
(click)="toggleFullscreen()"
>
{{ (fullscreen$ | async) ? 'Exit from fullscreen' : 'Fullscreen' }}
</button>
<button
class="remove-node-btn"
size="s"
tuiButton
type="button"
(click)="drawFlowStore.removeNode('node-3')"
>
Remove node-3
</button>
<div class="scale-controls">
<button
size="s"
tuiButton
type="button"
(click)="drawFlowStore.zoomOut()"
>
zoomOut
</button>
<tui-textfield
class="scale-field"
tuiTextfieldSize="m"
tuiTheme="light"
[tuiTextfieldCleaner]="false"
>
<label tuiLabel>Set scale</label>
<input
aria-label="Set scale"
tuiInputNumber
[formControl]="scaleControl"
[max]="panZoomOptions.maxZoom"
[min]="panZoomOptions.minZoom"
[step]="0.05"
/>
</tui-textfield>
<div class="scale-value">{{ drawFlowStore.scale$ | async }}%</div>
<button
size="s"
tuiButton
type="button"
(click)="drawFlowStore.zoomIn()"
>
zoomIn
</button>
</div>
<div class="selection-info">
<ng-container *ngIf="drawFlowStore.selectedNode() as node; else connectionInfo">
<div class="selection-title">Selected node</div>
<div class="selection-value">{{ node.id }}</div>
</ng-container>
<ng-template #connectionInfo>
<ng-container
*ngIf="
drawFlowStore.selectedConnection() as connection;
else noSelection
"
>
<div class="selection-title">Selected connection</div>
<div class="selection-value">{{ connection.source.nodeId }} → {{ connection.target.nodeId }}</div>
</ng-container>
</ng-template>
<ng-template #noSelection>
<div class="selection-title">Nothing selected</div>
</ng-template>
</div>
</div>