Skip to content

Commit

Permalink
docs: Added UndoRedo example
Browse files Browse the repository at this point in the history
  • Loading branch information
siarheihuzarevich committed Jan 31, 2025
1 parent a0b5bdf commit 4053600
Show file tree
Hide file tree
Showing 17 changed files with 378 additions and 32 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"@angular/router": "^18.2.0",
"@angular/ssr": "^18.2.3",
"@cypress/angular": "^2.1.0",
"@foblex/2d": "1.1.8",
"@foblex/2d": "^1.1.9",
"@foblex/drag-toolkit": "1.1.0",
"@foblex/m-render": "2.5.4",
"@foblex/mediator": "1.1.2",
Expand Down
61 changes: 61 additions & 0 deletions projects/f-examples/advanced/undo-redo/undo-redo.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<f-flow fDraggable (fLoaded)="onLoaded()"
(fCreateConnection)="onConnectionCreated($event)"
(fReassignConnection)="onConnectionReassigned($event)">
<f-canvas fZoom [scale]="viewModel.scale" [position]="viewModel.position">
<f-connection-for-create fBehavior="fixed" fType="segment">
<svg viewBox="0 0 700 700" fMarker [type]="eMarkerType.START" class="connection-marker"
[height]="5" [width]="5"
[refX]="2.5" [refY]="2.5" markerUnits="strokeWidth">
<circle cx="350" cy="350" r="350"/>
</svg>
<svg viewBox="0 0 6 7" fMarker [type]="eMarkerType.END" class="connection-marker"
[height]="7" [width]="6"
[refX]="5.5" [refY]="3.5" markerUnits="strokeWidth" orient="auto">
<path d="M0.000391006 0L6 3.5L0.000391006 7L0.000391006 0Z"/>
</svg>

</f-connection-for-create>
<f-snap-connection [fSnapThreshold]="100" fBehavior="fixed" fType="segment">
<svg viewBox="0 0 700 700" fMarker [type]="eMarkerType.START" class="connection-marker"
[height]="5" [width]="5"
[refX]="2.5" [refY]="2.5" markerUnits="strokeWidth">
<circle cx="350" cy="350" r="350"/>
</svg>
<svg viewBox="0 0 6 7" fMarker [type]="eMarkerType.END" class="connection-marker"
[height]="7" [width]="6"
[refX]="5.5" [refY]="3.5" markerUnits="strokeWidth" orient="auto">
<path d="M0.000391006 0L6 3.5L0.000391006 7L0.000391006 0Z"/>
</svg>

</f-snap-connection>
@for (connection of viewModel.connections; track connection.id) {
<f-connection [fConnectionId]="connection.id"
[fOutputId]="connection.source"
[fInputId]="connection.target" fBehavior="fixed" fType="segment" [fSelectionDisabled]="true">
<svg viewBox="0 0 700 700" fMarker [type]="eMarkerType.START" class="connection-marker"
[height]="5" [width]="5"
[refX]="2.5" [refY]="2.5" markerUnits="strokeWidth">
<circle cx="350" cy="350" r="350"/>
</svg>
<svg viewBox="0 0 6 7" fMarker [type]="eMarkerType.END" class="connection-marker"
[height]="7" [width]="6"
[refX]="5.5" [refY]="3.5" markerUnits="strokeWidth" orient="auto">
<path d="M0.000391006 0L6 3.5L0.000391006 7L0.000391006 0Z"/>
</svg>
</f-connection>
}
@for (node of viewModel.nodes; track node.id) {
<div fNode [fNodePosition]="node.position"
fDragHandle (fNodePositionChange)="onNodeChanged(node.id, $event)">{{ node.text }}
<div fNodeInput fInputConnectableSide="left" class="left"></div>
<div fNodeOutput [isSelfConnectable]="false" fOutputConnectableSide="top" class="top"></div>
<div fNodeInput fInputConnectableSide="right" class="right"></div>
<div fNodeOutput [isSelfConnectable]="false" fOutputConnectableSide="bottom" class="bottom"></div>
</div>
}
</f-canvas>
</f-flow>
<div class="toolbar">
<button class="f-button" (click)="onUndoClick()" [disabled]="!isUndoEnabled">Undo</button>
<button class="f-button" (click)="onRedoClick()" [disabled]="!isRedoEnabled">Redo</button>
</div>
45 changes: 45 additions & 0 deletions projects/f-examples/advanced/undo-redo/undo-redo.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
@use "../../flow-common";

::ng-deep undo-redo {
@include flow-common.connection;

.connection-marker {
circle, rect, path {
fill: var(--connection-color);
}
}

.f-connection {
&.f-selected {

.f-connection-path {
stroke: var(--minimap-node-selected-color);
}
.connection-marker {
circle, rect, path {
fill: var(--minimap-node-selected-color);
}
}
}
}
}

.f-node {
@include flow-common.node;
}

.f-node-input, .f-node-output {
&:not(.f-node) {
@include flow-common.connectors;
}
}

.f-node-output {
&:not(.f-node) {
border-radius: 4px;
}
}

.toolbar {
@include flow-common.toolbar;
}
181 changes: 181 additions & 0 deletions projects/f-examples/advanced/undo-redo/undo-redo.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
inject,
OnInit,
ViewChild
} from '@angular/core';
import {
EFMarkerType,
FCanvasChangeEvent,
FCanvasComponent,
FCreateConnectionEvent,
FFlowModule,
FReassignConnectionEvent
} from '@foblex/flow';
import { IPoint } from '@foblex/2d';
import { generateGuid } from '@foblex/utils';
import { debounceTime } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

interface INode {
id: string;
position: IPoint;
text: string;
}

interface IConnection {
id: string;
source: string;
target: string;
}

interface IState {
scale?: number;
position?: IPoint;
nodes: INode[];
connections: IConnection[];
}

const STORE: IState = {
scale: 1,
position: { x: 0, y: 0 },
nodes: [ {
id: '1',
position: { x: 0, y: 200 },
text: 'Node 1',
}, {
id: '2',
position: { x: 200, y: 200 },
text: 'Node 2',
} ],
connections: [ {
id: '1',
source: 'f-node-output-0',
target: 'f-node-input-2',
} ],
};

@Component({
selector: 'undo-redo',
styleUrls: [ './undo-redo.component.scss' ],
templateUrl: './undo-redo.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
FFlowModule,
]
})
export class UndoRedoComponent implements OnInit {

private _destroyRef = inject(DestroyRef);
private _changeDetectorRef = inject(ChangeDetectorRef);

private _undoStates: IState[] = [];
private _redoStates: IState[] = [];

protected isRedoEnabled: boolean = false;
protected isUndoEnabled: boolean = false;

@ViewChild(FCanvasComponent, { static: true })
protected fCanvas!: FCanvasComponent;

protected viewModel: IState = STORE;

protected readonly eMarkerType = EFMarkerType;

private _isFirstCanvasChange: boolean = true;

public ngOnInit(): void {
this._subscribeToCanvasChange();
}

protected onLoaded(): void {
this.fCanvas.resetScaleAndCenter(false);
}

private _subscribeToCanvasChange(): void {
this.fCanvas.fCanvasChange.pipe(
takeUntilDestroyed(this._destroyRef), debounceTime(200)
).subscribe((event) => {
if (this._isFirstCanvasChange) {
this._setCenteredFlowAsDefault(event);
return;
}

this._stateChanged();

this.viewModel.position = event.position;
this.viewModel.scale = event.scale;
});
}

private _setCenteredFlowAsDefault(event: FCanvasChangeEvent): void {
this._isFirstCanvasChange = false;
this.viewModel.position = event.position;
this.viewModel.scale = event.scale;
}

protected onConnectionCreated(event: FCreateConnectionEvent): void {
if (event.fInputId) {
this._stateChanged();
this._createConnection(event.fOutputId, event.fInputId);
}
}

protected onConnectionReassigned(event: FReassignConnectionEvent): void {
if (event.newFInputId) {
this._stateChanged();
this._removeConnection(event.fConnectionId);
this._createConnection(event.fOutputId, event.newFInputId);
}
}

protected onNodeChanged(nodeId: string, position: IPoint): void {
this._stateChanged();
const node = this.viewModel.nodes.find((x) => x.id === nodeId);
if (node) {
node.position = position;
}
}

private _removeConnection(connectionId: string): void {
const index = this.viewModel.connections.findIndex((x) => x.id === connectionId);
this.viewModel.connections.splice(index, 1);
}

private _createConnection(source: string, target: string): void {
this.viewModel.connections.push({ id: generateGuid(), source, target });
}

private _stateChanged(): void {
this._undoStates.push(this._deepClone(this.viewModel));
this._redoStates = [];
this._afterStateChanged();
this._changeDetectorRef.markForCheck();
}

protected onUndoClick(): void {
const currentState = this._deepClone(this.viewModel);
this.viewModel = this._deepClone(this._undoStates.pop()!);
this._redoStates.push(currentState);
this._afterStateChanged();
}

protected onRedoClick(): void {
this._undoStates.push(this._deepClone(this.viewModel));
this.viewModel = this._deepClone(this._redoStates.pop()!);
this._afterStateChanged();
}

private _afterStateChanged(): void {
this.isRedoEnabled = this._redoStates.length > 0;
this.isUndoEnabled = this._undoStates.length > 0;
}

private _deepClone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}
}
2 changes: 1 addition & 1 deletion projects/f-flow/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"@angular/core": ">=15.0.0",
"@foblex/platform": "1.0.4",
"@foblex/mediator": "1.1.2",
"@foblex/2d": "1.1.8",
"@foblex/2d": "1.1.9",
"@foblex/drag-toolkit": "1.1.0",
"@foblex/utils": "1.1.0"
},
Expand Down
18 changes: 9 additions & 9 deletions projects/f-flow/src/f-canvas/f-canvas.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ export class FCanvasComponent extends FCanvasBase implements OnInit, OnDestroy {

@Input()
public set position(value: IPoint | undefined) {
this._fMediator.send(new InputCanvasPositionRequest(this.transform, value));
this._fMediator.execute(new InputCanvasPositionRequest(this.transform, PointExtensions.castToPoint(value)));
}

@Input()
public set scale(value: number | undefined) {
this._fMediator.send(new InputCanvasScaleRequest(this.transform, value));
this._fMediator.execute(new InputCanvasScaleRequest(this.transform, value));
}

public override get hostElement(): HTMLElement {
Expand All @@ -67,31 +67,31 @@ export class FCanvasComponent extends FCanvasBase implements OnInit, OnDestroy {
private _fMediator = inject(FMediator);

public ngOnInit() {
this._fMediator.send(new AddCanvasToStoreRequest(this));
this._fMediator.execute(new AddCanvasToStoreRequest(this));
}

public override redraw(): void {
this._fMediator.send(new SetBackgroundTransformRequest(this.transform));
this._fMediator.execute(new SetBackgroundTransformRequest(this.transform));
this.hostElement.setAttribute("style", `transform: ${ TransformModelExtensions.toString(this.transform) }`);
this._fMediator.send(new NotifyTransformChangedRequest());
this._fMediator.execute(new NotifyTransformChangedRequest());
}

public override redrawWithAnimation(): void {
this._fMediator.send(new SetBackgroundTransformRequest(this.transform));
this._fMediator.execute(new SetBackgroundTransformRequest(this.transform));
this.hostElement.setAttribute("style", `transition: transform ${ isMobile() ? 80 : 150 }ms ease-in-out; transform: ${ TransformModelExtensions.toString(this.transform) }`);
transitionEnd(this.hostElement, () => this.redraw());
}

public centerGroupOrNode(id: string, animated: boolean = true): void {
this._fMediator.send(new CenterGroupOrNodeRequest(id, animated));
this._fMediator.execute(new CenterGroupOrNodeRequest(id, animated));
}

public fitToScreen(toCenter: IPoint = PointExtensions.initialize(), animated: boolean = true): void {
this._fMediator.send(new FitToFlowRequest(toCenter, animated));
this._fMediator.execute(new FitToFlowRequest(toCenter, animated));
}

public resetScaleAndCenter(animated: boolean = true): void {
this._fMediator.send(new ResetScaleAndCenterRequest(animated));
this._fMediator.execute(new ResetScaleAndCenterRequest(animated));
}

public getScale(): number {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export class ReassignConnectionDragHandler implements IDraggableItem<ICreateReas
}

public onPointerUp(): void {
this._drawConnection(this._fInputWithRect.fRect, this._fInputWithRect.fConnector.fConnectableSide);
this._drawConnection(this._toConnectorRect, this._fInputWithRect.fConnector.fConnectableSide);
this._fSnapConnection?.hide();

this._fMediator.execute(
Expand Down
Loading

0 comments on commit 4053600

Please sign in to comment.