From ff8ae5bd5a624347d81da925917ca537b02b0378 Mon Sep 17 00:00:00 2001 From: Siarhei Huzarevich Date: Mon, 13 Jan 2025 21:03:47 +0100 Subject: [PATCH 1/2] feat: added grid system to fDraggable directive --- .../grid-system-example.component.html | 10 +++ .../grid-system-example.component.scss | 10 +++ .../grid-system-example.component.ts | 22 +++++ .../get-node-padding.execution.ts | 0 .../get-node-padding.request.ts | 0 .../f-node}/get-node-padding/index.ts | 0 .../get-parent-nodes.execution.ts | 15 ++-- .../get-parent-nodes.request.ts | 0 .../f-node}/get-parent-nodes/index.ts | 0 projects/f-flow/src/domain/f-node/index.ts | 4 + .../f-flow/src/domain/f-node/providers.ts | 6 ++ .../get-normalized-node-rect.execution.ts | 26 ------ .../get-normalized-node-rect.request.ts | 9 -- .../domain/get-normalized-node-rect/index.ts | 3 - ...t-normalized-parent-node-rect.execution.ts | 18 ++-- .../f-flow/src/f-draggable/domain/index.ts | 6 -- .../is-array-has-parent-node.execution.ts | 2 +- .../is-connection-under-node.request.ts | 1 - .../src/f-draggable/domain/providers.ts | 9 -- .../src/f-draggable/f-draggable-base.ts | 4 + .../src/f-draggable/f-draggable.directive.ts | 19 +++-- .../get-node-resize-restrictions.execution.ts | 3 +- ...ormalized-children-nodes-rect.execution.ts | 7 +- .../node-resize/node-resize.drag-handler.ts | 4 +- ...des-drag-model-from-selection.execution.ts | 57 +++++++------ .../get-node-move-restrictions.execution.ts | 18 ++-- .../node-move-finalize.execution.ts | 66 +++++++++------ .../node-move-preparation.execution.ts | 82 ++++++++++--------- .../node-move-preparation.validator.ts | 31 ++++--- .../src/f-draggable/node/node.drag-handler.ts | 34 ++++++-- projects/f-flow/src/f-node/domain/index.ts | 1 - .../f-drag-handle.directive.ts | 2 +- .../f-flow/src/f-node/f-drag-handle/index.ts | 1 - projects/f-flow/src/f-node/index.ts | 8 +- .../f-flow/src/f-node/{domain => }/is-node.ts | 0 projects/f-flow/src/f-node/providers.ts | 2 +- 36 files changed, 261 insertions(+), 219 deletions(-) create mode 100644 projects/f-examples/extensions/grid-system-example/grid-system-example.component.html create mode 100644 projects/f-examples/extensions/grid-system-example/grid-system-example.component.scss create mode 100644 projects/f-examples/extensions/grid-system-example/grid-system-example.component.ts rename projects/f-flow/src/{f-draggable/domain => domain/f-node}/get-node-padding/get-node-padding.execution.ts (100%) rename projects/f-flow/src/{f-draggable/domain => domain/f-node}/get-node-padding/get-node-padding.request.ts (100%) rename projects/f-flow/src/{f-draggable/domain => domain/f-node}/get-node-padding/index.ts (100%) rename projects/f-flow/src/{f-draggable/domain => domain/f-node}/get-parent-nodes/get-parent-nodes.execution.ts (62%) rename projects/f-flow/src/{f-draggable/domain => domain/f-node}/get-parent-nodes/get-parent-nodes.request.ts (100%) rename projects/f-flow/src/{f-draggable/domain => domain/f-node}/get-parent-nodes/index.ts (100%) delete mode 100644 projects/f-flow/src/f-draggable/domain/get-normalized-node-rect/get-normalized-node-rect.execution.ts delete mode 100644 projects/f-flow/src/f-draggable/domain/get-normalized-node-rect/get-normalized-node-rect.request.ts delete mode 100644 projects/f-flow/src/f-draggable/domain/get-normalized-node-rect/index.ts delete mode 100644 projects/f-flow/src/f-node/domain/index.ts rename projects/f-flow/src/f-node/{f-drag-handle => }/f-drag-handle.directive.ts (90%) delete mode 100644 projects/f-flow/src/f-node/f-drag-handle/index.ts rename projects/f-flow/src/f-node/{domain => }/is-node.ts (100%) diff --git a/projects/f-examples/extensions/grid-system-example/grid-system-example.component.html b/projects/f-examples/extensions/grid-system-example/grid-system-example.component.html new file mode 100644 index 0000000..f56e139 --- /dev/null +++ b/projects/f-examples/extensions/grid-system-example/grid-system-example.component.html @@ -0,0 +1,10 @@ + + + + + + +
I'm a node
+
I'm a node
+
+
diff --git a/projects/f-examples/extensions/grid-system-example/grid-system-example.component.scss b/projects/f-examples/extensions/grid-system-example/grid-system-example.component.scss new file mode 100644 index 0000000..e31db81 --- /dev/null +++ b/projects/f-examples/extensions/grid-system-example/grid-system-example.component.scss @@ -0,0 +1,10 @@ +@use "../../flow-common"; + +::ng-deep grid-system-example { + @include flow-common.connection; +} + +.f-node { + @include flow-common.node; +} + diff --git a/projects/f-examples/extensions/grid-system-example/grid-system-example.component.ts b/projects/f-examples/extensions/grid-system-example/grid-system-example.component.ts new file mode 100644 index 0000000..7f55022 --- /dev/null +++ b/projects/f-examples/extensions/grid-system-example/grid-system-example.component.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, Component, ViewChild } from '@angular/core'; +import { FCanvasComponent, FFlowModule } from '@foblex/flow'; + +@Component({ + selector: 'grid-system-example', + styleUrls: [ './grid-system-example.component.scss' ], + templateUrl: './grid-system-example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + FFlowModule, + ] +}) +export class GridSystemExampleComponent { + + @ViewChild(FCanvasComponent, { static: true }) + public fCanvas!: FCanvasComponent; + + public onLoaded(): void { + this.fCanvas.resetScaleAndCenter(false); + } +} diff --git a/projects/f-flow/src/f-draggable/domain/get-node-padding/get-node-padding.execution.ts b/projects/f-flow/src/domain/f-node/get-node-padding/get-node-padding.execution.ts similarity index 100% rename from projects/f-flow/src/f-draggable/domain/get-node-padding/get-node-padding.execution.ts rename to projects/f-flow/src/domain/f-node/get-node-padding/get-node-padding.execution.ts diff --git a/projects/f-flow/src/f-draggable/domain/get-node-padding/get-node-padding.request.ts b/projects/f-flow/src/domain/f-node/get-node-padding/get-node-padding.request.ts similarity index 100% rename from projects/f-flow/src/f-draggable/domain/get-node-padding/get-node-padding.request.ts rename to projects/f-flow/src/domain/f-node/get-node-padding/get-node-padding.request.ts diff --git a/projects/f-flow/src/f-draggable/domain/get-node-padding/index.ts b/projects/f-flow/src/domain/f-node/get-node-padding/index.ts similarity index 100% rename from projects/f-flow/src/f-draggable/domain/get-node-padding/index.ts rename to projects/f-flow/src/domain/f-node/get-node-padding/index.ts diff --git a/projects/f-flow/src/f-draggable/domain/get-parent-nodes/get-parent-nodes.execution.ts b/projects/f-flow/src/domain/f-node/get-parent-nodes/get-parent-nodes.execution.ts similarity index 62% rename from projects/f-flow/src/f-draggable/domain/get-parent-nodes/get-parent-nodes.execution.ts rename to projects/f-flow/src/domain/f-node/get-parent-nodes/get-parent-nodes.execution.ts index 54f8c82..3b6a93c 100644 --- a/projects/f-flow/src/f-draggable/domain/get-parent-nodes/get-parent-nodes.execution.ts +++ b/projects/f-flow/src/domain/f-node/get-parent-nodes/get-parent-nodes.execution.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { GetParentNodesRequest } from './get-parent-nodes.request'; import { FExecutionRegister, IExecution } from '@foblex/mediator'; import { FNodeBase } from '../../../f-node'; @@ -9,27 +9,24 @@ import { FComponentsStore } from '../../../f-storage'; export class GetParentNodesExecution implements IExecution { - constructor( - private fComponentsStore: FComponentsStore - ) { - } + private _fComponentsStore = inject(FComponentsStore); public handle(request: GetParentNodesRequest): FNodeBase[] { - return this.getParentNodes(request.fNode, new Set(), []); + return this._getParentNodes(request.fNode, new Set(), []); } - private getParentNodes(fNode: FNodeBase, visited: Set, result: FNodeBase[]): FNodeBase[] { + private _getParentNodes(fNode: FNodeBase, visited: Set, result: FNodeBase[]): FNodeBase[] { if (visited.has(fNode.fId)) { throw new Error('Circular reference detected in the node hierarchy. Node id: ' + fNode.fId); } visited.add(fNode.fId); - const parent = this.fComponentsStore.fNodes.find((x) => x.fId === fNode.fParentId); + const parent = this._fComponentsStore.fNodes.find((x) => x.fId === fNode.fParentId); if (!parent) { return result; } result.push(parent); - return this.getParentNodes(parent, visited, result); + return this._getParentNodes(parent, visited, result); } } diff --git a/projects/f-flow/src/f-draggable/domain/get-parent-nodes/get-parent-nodes.request.ts b/projects/f-flow/src/domain/f-node/get-parent-nodes/get-parent-nodes.request.ts similarity index 100% rename from projects/f-flow/src/f-draggable/domain/get-parent-nodes/get-parent-nodes.request.ts rename to projects/f-flow/src/domain/f-node/get-parent-nodes/get-parent-nodes.request.ts diff --git a/projects/f-flow/src/f-draggable/domain/get-parent-nodes/index.ts b/projects/f-flow/src/domain/f-node/get-parent-nodes/index.ts similarity index 100% rename from projects/f-flow/src/f-draggable/domain/get-parent-nodes/index.ts rename to projects/f-flow/src/domain/f-node/get-parent-nodes/index.ts diff --git a/projects/f-flow/src/domain/f-node/index.ts b/projects/f-flow/src/domain/f-node/index.ts index e8ff167..3acd809 100644 --- a/projects/f-flow/src/domain/f-node/index.ts +++ b/projects/f-flow/src/domain/f-node/index.ts @@ -4,8 +4,12 @@ export * from './calculate-nodes-bounding-box'; export * from './calculate-nodes-bounding-box-normalized-position'; +export * from './get-node-padding'; + export * from './get-nodes'; +export * from './get-parent-nodes'; + export * from './update-node-when-state-or-size-changed'; export * from './remove-node-from-store'; diff --git a/projects/f-flow/src/domain/f-node/providers.ts b/projects/f-flow/src/domain/f-node/providers.ts index 590f495..8efc03c 100644 --- a/projects/f-flow/src/domain/f-node/providers.ts +++ b/projects/f-flow/src/domain/f-node/providers.ts @@ -6,6 +6,8 @@ import { CalculateNodesBoundingBoxExecution } from './calculate-nodes-bounding-b import { CalculateNodesBoundingBoxNormalizedPositionExecution } from './calculate-nodes-bounding-box-normalized-position'; +import { GetNodePaddingExecution } from './get-node-padding'; +import { GetParentNodesExecution } from './get-parent-nodes'; export const F_NODE_FEATURES = [ @@ -15,8 +17,12 @@ export const F_NODE_FEATURES = [ CalculateNodesBoundingBoxNormalizedPositionExecution, + GetNodePaddingExecution, + GetNodesExecution, + GetParentNodesExecution, + UpdateNodeWhenStateOrSizeChangedExecution, RemoveNodeFromStoreExecution diff --git a/projects/f-flow/src/f-draggable/domain/get-normalized-node-rect/get-normalized-node-rect.execution.ts b/projects/f-flow/src/f-draggable/domain/get-normalized-node-rect/get-normalized-node-rect.execution.ts deleted file mode 100644 index 0d55dd7..0000000 --- a/projects/f-flow/src/f-draggable/domain/get-normalized-node-rect/get-normalized-node-rect.execution.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Injectable } from '@angular/core'; -import { GetNormalizedNodeRectRequest } from './get-normalized-node-rect.request'; -import { IPoint, IRect, RectExtensions } from '@foblex/2d'; -import { FExecutionRegister, IExecution } from '@foblex/mediator'; -import { FComponentsStore } from '../../../f-storage'; - -@Injectable() -@FExecutionRegister(GetNormalizedNodeRectRequest) -export class GetNormalizedNodeRectExecution - implements IExecution { - - constructor( - private fComponentsStore: FComponentsStore - ) { - } - - public handle(request: GetNormalizedNodeRectRequest): IRect { - return this.normalizeRect(RectExtensions.fromElement(request.fNode.hostElement), request.fNode.position); - } - - private normalizeRect(scaledRect: IRect, position: IPoint): IRect { - const rect = RectExtensions.div(scaledRect, this.fComponentsStore.fCanvas!.transform.scale); - - return RectExtensions.initialize(position.x, position.y, rect.width, rect.height); - } -} diff --git a/projects/f-flow/src/f-draggable/domain/get-normalized-node-rect/get-normalized-node-rect.request.ts b/projects/f-flow/src/f-draggable/domain/get-normalized-node-rect/get-normalized-node-rect.request.ts deleted file mode 100644 index e85dd3b..0000000 --- a/projects/f-flow/src/f-draggable/domain/get-normalized-node-rect/get-normalized-node-rect.request.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { FNodeBase } from '../../../f-node'; - -export class GetNormalizedNodeRectRequest { - - constructor( - public fNode: FNodeBase - ) { - } -} diff --git a/projects/f-flow/src/f-draggable/domain/get-normalized-node-rect/index.ts b/projects/f-flow/src/f-draggable/domain/get-normalized-node-rect/index.ts deleted file mode 100644 index c444ed8..0000000 --- a/projects/f-flow/src/f-draggable/domain/get-normalized-node-rect/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './get-normalized-node-rect.execution'; - -export * from './get-normalized-node-rect.request'; diff --git a/projects/f-flow/src/f-draggable/domain/get-normalized-parent-node-rect/get-normalized-parent-node-rect.execution.ts b/projects/f-flow/src/f-draggable/domain/get-normalized-parent-node-rect/get-normalized-parent-node-rect.execution.ts index e86ce5e..54cba89 100644 --- a/projects/f-flow/src/f-draggable/domain/get-normalized-parent-node-rect/get-normalized-parent-node-rect.execution.ts +++ b/projects/f-flow/src/f-draggable/domain/get-normalized-parent-node-rect/get-normalized-parent-node-rect.execution.ts @@ -1,11 +1,11 @@ import { Injectable } from '@angular/core'; import { GetNormalizedParentNodeRectRequest } from './get-normalized-parent-node-rect.request'; import { IRect, RectExtensions } from '@foblex/2d'; -import { GetNormalizedNodeRectRequest } from '../get-normalized-node-rect'; import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; import { FNodeBase } from '../../../f-node'; import { FComponentsStore } from '../../../f-storage'; -import { GetNodePaddingRequest } from '../get-node-padding'; +import { GetNodePaddingRequest } from '../../../domain'; +import { GetNormalizedElementRectRequest } from '../../../domain'; @Injectable() @FExecutionRegister(GetNormalizedParentNodeRectRequest) @@ -32,18 +32,18 @@ export class GetNormalizedParentNodeRectExecution } private getParentRect(node: FNodeBase): IRect { - const rect = this.getNormalizedNodeRect(node); + const rect = this._getNodeRect(node); const padding = this.getNodePadding(node, rect); return RectExtensions.initialize( - rect.x + padding[0], - rect.y + padding[1], - rect.width - padding[0] - padding[2], - rect.height - padding[1] - padding[3] + rect.x + padding[ 0 ], + rect.y + padding[ 1 ], + rect.width - padding[ 0 ] - padding[ 2 ], + rect.height - padding[ 1 ] - padding[ 3 ] ); } - private getNormalizedNodeRect(node: FNodeBase): IRect { - return this.fMediator.send(new GetNormalizedNodeRectRequest(node)); + private _getNodeRect(fNode: FNodeBase): IRect { + return this.fMediator.send(new GetNormalizedElementRectRequest(fNode.hostElement)); } private getNodePadding(node: FNodeBase, rect: IRect): [ number, number, number, number ] { diff --git a/projects/f-flow/src/f-draggable/domain/index.ts b/projects/f-flow/src/f-draggable/domain/index.ts index a5d4d32..5c2178c 100644 --- a/projects/f-flow/src/f-draggable/domain/index.ts +++ b/projects/f-flow/src/f-draggable/domain/index.ts @@ -1,11 +1,5 @@ -export * from './get-node-padding'; - -export * from './get-normalized-node-rect'; - export * from './get-normalized-parent-node-rect'; -export * from './get-parent-nodes'; - export * from './is-array-has-parent-node'; export * from './is-connection-under-node'; diff --git a/projects/f-flow/src/f-draggable/domain/is-array-has-parent-node/is-array-has-parent-node.execution.ts b/projects/f-flow/src/f-draggable/domain/is-array-has-parent-node/is-array-has-parent-node.execution.ts index 250e2b8..b36e8f4 100644 --- a/projects/f-flow/src/f-draggable/domain/is-array-has-parent-node/is-array-has-parent-node.execution.ts +++ b/projects/f-flow/src/f-draggable/domain/is-array-has-parent-node/is-array-has-parent-node.execution.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { IsArrayHasParentNodeRequest } from './is-array-has-parent-node.request'; import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; import { FNodeBase } from '../../../f-node'; -import { GetParentNodesRequest } from '../get-parent-nodes'; +import { GetParentNodesRequest } from '../../../domain/f-node/get-parent-nodes'; @Injectable() @FExecutionRegister(IsArrayHasParentNodeRequest) diff --git a/projects/f-flow/src/f-draggable/domain/is-connection-under-node/is-connection-under-node.request.ts b/projects/f-flow/src/f-draggable/domain/is-connection-under-node/is-connection-under-node.request.ts index e1919f7..87088c5 100644 --- a/projects/f-flow/src/f-draggable/domain/is-connection-under-node/is-connection-under-node.request.ts +++ b/projects/f-flow/src/f-draggable/domain/is-connection-under-node/is-connection-under-node.request.ts @@ -1,3 +1,2 @@ export class IsConnectionUnderNodeRequest { - } diff --git a/projects/f-flow/src/f-draggable/domain/providers.ts b/projects/f-flow/src/f-draggable/domain/providers.ts index 5c4b244..e052299 100644 --- a/projects/f-flow/src/f-draggable/domain/providers.ts +++ b/projects/f-flow/src/f-draggable/domain/providers.ts @@ -1,20 +1,11 @@ -import { GetNormalizedNodeRectExecution } from './get-normalized-node-rect'; -import { GetParentNodesExecution } from './get-parent-nodes'; import { IsArrayHasParentNodeExecution } from './is-array-has-parent-node'; import { GetNormalizedParentNodeRectExecution } from './get-normalized-parent-node-rect'; -import { GetNodePaddingExecution } from './get-node-padding'; import { IsConnectionUnderNodeExecution, IsConnectionUnderNodeValidator } from './is-connection-under-node'; export const DRAG_AND_DROP_COMMON_PROVIDERS = [ - GetNodePaddingExecution, - - GetNormalizedNodeRectExecution, - GetNormalizedParentNodeRectExecution, - GetParentNodesExecution, - IsArrayHasParentNodeExecution, IsConnectionUnderNodeExecution, diff --git a/projects/f-flow/src/f-draggable/f-draggable-base.ts b/projects/f-flow/src/f-draggable/f-draggable-base.ts index 123f4cd..0b4a168 100644 --- a/projects/f-flow/src/f-draggable/f-draggable-base.ts +++ b/projects/f-flow/src/f-draggable/f-draggable-base.ts @@ -21,6 +21,10 @@ export abstract class FDraggableBase extends DragAndDropBase { public abstract fDropToGroup: EventEmitter; + public abstract vCellSize: number; + + public abstract hCellSize: number; + protected constructor( ngZone: ICanRunOutsideAngular | undefined ) { diff --git a/projects/f-flow/src/f-draggable/f-draggable.directive.ts b/projects/f-flow/src/f-draggable/f-draggable.directive.ts index 4a03e49..e7ceb9e 100644 --- a/projects/f-flow/src/f-draggable/f-draggable.directive.ts +++ b/projects/f-flow/src/f-draggable/f-draggable.directive.ts @@ -66,22 +66,29 @@ export class FDraggableDirective extends FDraggableBase implements OnInit, After } @Output() - public override fSelectionChange: EventEmitter = new EventEmitter(); + public override fSelectionChange = new EventEmitter(); @Output() - public override fNodeIntersectedWithConnections: EventEmitter = new EventEmitter(); + public override fNodeIntersectedWithConnections = new EventEmitter(); @Output() - public override fCreateNode: EventEmitter = new EventEmitter(); + public override fCreateNode = new EventEmitter(); @Output() - public override fReassignConnection: EventEmitter = new EventEmitter(); + public override fReassignConnection = new EventEmitter(); @Output() - public override fCreateConnection: EventEmitter = new EventEmitter(); + public override fCreateConnection = new EventEmitter(); @Output() - public override fDropToGroup: EventEmitter = new EventEmitter(); + public override fDropToGroup = new EventEmitter(); + + @Input() + public override vCellSize = 1; + + + @Input() + public override hCellSize = 1; @ContentChildren(F_DRAG_AND_DROP_PLUGIN, { descendants: true }) private plugins!: QueryList; diff --git a/projects/f-flow/src/f-draggable/node-resize/get-node-resize-restrictions/get-node-resize-restrictions.execution.ts b/projects/f-flow/src/f-draggable/node-resize/get-node-resize-restrictions/get-node-resize-restrictions.execution.ts index 09f01cb..61acd2f 100644 --- a/projects/f-flow/src/f-draggable/node-resize/get-node-resize-restrictions/get-node-resize-restrictions.execution.ts +++ b/projects/f-flow/src/f-draggable/node-resize/get-node-resize-restrictions/get-node-resize-restrictions.execution.ts @@ -3,9 +3,10 @@ import { GetNodeResizeRestrictionsRequest } from './get-node-resize-restrictions import { IRect, SizeExtensions } from '@foblex/2d'; import { INodeResizeRestrictions } from './i-node-resize-restrictions'; import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; -import { GetNodePaddingRequest, GetNormalizedParentNodeRectRequest } from '../../domain'; +import { GetNormalizedParentNodeRectRequest } from '../../domain'; import { GetNormalizedChildrenNodesRectRequest } from '../get-normalized-children-nodes-rect'; import { FNodeBase } from '../../../f-node'; +import { GetNodePaddingRequest } from '../../../domain'; @Injectable() diff --git a/projects/f-flow/src/f-draggable/node-resize/get-normalized-children-nodes-rect/get-normalized-children-nodes-rect.execution.ts b/projects/f-flow/src/f-draggable/node-resize/get-normalized-children-nodes-rect/get-normalized-children-nodes-rect.execution.ts index 15af09b..87d6058 100644 --- a/projects/f-flow/src/f-draggable/node-resize/get-normalized-children-nodes-rect/get-normalized-children-nodes-rect.execution.ts +++ b/projects/f-flow/src/f-draggable/node-resize/get-normalized-children-nodes-rect/get-normalized-children-nodes-rect.execution.ts @@ -3,8 +3,7 @@ import { GetNormalizedChildrenNodesRectRequest } from './get-normalized-children import { IRect, RectExtensions } from '@foblex/2d'; import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; import { FNodeBase } from '../../../f-node'; -import { GetNormalizedNodeRectRequest } from '../../domain'; -import { GetDeepChildrenNodesAndGroupsRequest } from '../../../domain'; +import { GetDeepChildrenNodesAndGroupsRequest, GetNormalizedElementRectRequest } from '../../../domain'; @Injectable() @FExecutionRegister(GetNormalizedChildrenNodesRectRequest) @@ -28,8 +27,8 @@ export class GetNormalizedChildrenNodesRectExecution return this.fMediator.send(new GetDeepChildrenNodesAndGroupsRequest(fId)); } - private normalizeRect(node: FNodeBase): IRect { - return this.fMediator.send(new GetNormalizedNodeRectRequest(node)); + private normalizeRect(fNode: FNodeBase): IRect { + return this.fMediator.send(new GetNormalizedElementRectRequest(fNode.hostElement)); } private concatRectWithParentPadding(rect: IRect, padding: [ number, number, number, number ]): IRect { diff --git a/projects/f-flow/src/f-draggable/node-resize/node-resize.drag-handler.ts b/projects/f-flow/src/f-draggable/node-resize/node-resize.drag-handler.ts index d02ce24..e1c6464 100644 --- a/projects/f-flow/src/f-draggable/node-resize/node-resize.drag-handler.ts +++ b/projects/f-flow/src/f-draggable/node-resize/node-resize.drag-handler.ts @@ -2,12 +2,12 @@ import { IPoint, IRect, ISize, RectExtensions } from '@foblex/2d'; import { IDraggableItem } from '../i-draggable-item'; import { EFResizeHandleType, FNodeBase } from '../../f-node'; import { FMediator } from '@foblex/mediator'; -import { GetNormalizedNodeRectRequest } from '../domain'; import { GetNodeResizeRestrictionsRequest, INodeResizeRestrictions } from './get-node-resize-restrictions'; import { ApplyChildResizeRestrictionsRequest } from './apply-child-resize-restrictions'; import { CalculateChangedSizeRequest } from './calculate-changed-size'; import { CalculateChangedPositionRequest } from './calculate-changed-position'; import { ApplyParentResizeRestrictionsRequest } from './apply-parent-resize-restrictions'; +import { GetNormalizedElementRectRequest } from '../../domain'; export class NodeResizeDragHandler implements IDraggableItem { @@ -26,7 +26,7 @@ export class NodeResizeDragHandler implements IDraggableItem { } public prepareDragSequence(): void { - this.originalRect = this.fMediator.send(new GetNormalizedNodeRectRequest(this.fNode)); + this.originalRect = this.fMediator.send(new GetNormalizedElementRectRequest(this.fNode.hostElement)); this.restrictions = this.fMediator.send(new GetNodeResizeRestrictionsRequest(this.fNode, this.originalRect)); if (this.restrictions.childRect) { diff --git a/projects/f-flow/src/f-draggable/node/create-move-nodes-drag-model-from-selection/create-move-nodes-drag-model-from-selection.execution.ts b/projects/f-flow/src/f-draggable/node/create-move-nodes-drag-model-from-selection/create-move-nodes-drag-model-from-selection.execution.ts index a28c3ee..af696b4 100644 --- a/projects/f-flow/src/f-draggable/node/create-move-nodes-drag-model-from-selection/create-move-nodes-drag-model-from-selection.execution.ts +++ b/projects/f-flow/src/f-draggable/node/create-move-nodes-drag-model-from-selection/create-move-nodes-drag-model-from-selection.execution.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { CreateMoveNodesDragModelFromSelectionRequest } from './create-move-nodes-drag-model-from-selection.request'; import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; import { FComponentsStore } from '../../../f-storage'; @@ -14,8 +14,8 @@ import { PutInputConnectionHandlersToArrayRequest } from './domain/put-input-connection-handlers-to-array'; import { NodeResizeByChildDragHandler } from '../node-resize-by-child.drag-handler'; -import { GetParentNodesRequest, IsArrayHasParentNodeRequest } from '../../domain'; -import { GetDeepChildrenNodesAndGroupsRequest } from '../../../domain'; +import { IsArrayHasParentNodeRequest } from '../../domain'; +import { GetDeepChildrenNodesAndGroupsRequest, GetParentNodesRequest } from '../../../domain'; import { flatMap } from '@foblex/utils'; @Injectable() @@ -23,15 +23,14 @@ import { flatMap } from '@foblex/utils'; export class CreateMoveNodesDragModelFromSelectionExecution implements IExecution { - constructor( - private fComponentsStore: FComponentsStore, - private fDraggableDataContext: FDraggableDataContext, - private fMediator: FMediator - ) { - } + private _fMediator = inject(FMediator); + private _fComponentsStore = inject(FComponentsStore); + private _fDraggableDataContext = inject(FDraggableDataContext); public handle(request: CreateMoveNodesDragModelFromSelectionRequest): IDraggableItem[] { - const itemsToDrag = this.getNodesWithRestrictions(this.getSelectedNodes(request.nodeWithDisabledSelection)); + const itemsToDrag = this._getNodesWithRestrictions( + this._getSelectedNodes(request.nodeWithDisabledSelection) + ); return this.getDragHandlersWithConnections( this.getDragHandlersFromNodes(itemsToDrag), this.getAllOutputIds(itemsToDrag), @@ -39,27 +38,31 @@ export class CreateMoveNodesDragModelFromSelectionExecution ); } - private getSelectedNodes(nodeWithDisabledSelection?: FNodeBase): FNodeBase[] { - const result = this.fDraggableDataContext.selectedItems - .map((x) => this._findNode(x.hostElement)) - .filter((x): x is FNodeBase => !!x); + private _getSelectedNodes(nodeWithDisabledSelection?: FNodeBase): FNodeBase[] { + const result = this._getNodesFromSelection(); if(nodeWithDisabledSelection) { result.push(nodeWithDisabledSelection); } return result; } + private _getNodesFromSelection(): FNodeBase[] { + return this._fDraggableDataContext.selectedItems + .map((x) => this._findNode(x.hostElement)) + .filter((x): x is FNodeBase => !!x); + } + private _findNode(hostElement: HTMLElement | SVGElement): FNodeBase | undefined { - return this.fComponentsStore.fNodes.find(n => n.isContains(hostElement)); + return this._fComponentsStore.fNodes.find(n => n.isContains(hostElement)); } - private getNodesWithRestrictions(selectedNodes: FNodeBase[]): INodeWithDistanceRestrictions[] { + private _getNodesWithRestrictions(nodesToDrag: FNodeBase[]): INodeWithDistanceRestrictions[] { const result: INodeWithDistanceRestrictions[] = []; - selectedNodes.forEach((x) => { - const hasParentNodeInSelected = this.fMediator.send(new IsArrayHasParentNodeRequest(x, selectedNodes)); - const restrictions = this.fMediator.send(new GetNodeMoveRestrictionsRequest(x, hasParentNodeInSelected)); - const parentNodes = this.fMediator.send(new GetParentNodesRequest(x)); + nodesToDrag.forEach((x) => { + const hasParentNodeInSelected = this._fMediator.send(new IsArrayHasParentNodeRequest(x, nodesToDrag)); + const restrictions = this._fMediator.send(new GetNodeMoveRestrictionsRequest(x, hasParentNodeInSelected)); + const parentNodes = this._fMediator.send(new GetParentNodesRequest(x)); result.push({ node: x, parentNodes, ...restrictions }, ...this.getChildrenItemsToDrag(x, restrictions)); }); @@ -71,7 +74,7 @@ export class CreateMoveNodesDragModelFromSelectionExecution } private getChildrenNodes(fId: string): FNodeBase[] { - return this.fMediator.send(new GetDeepChildrenNodesAndGroupsRequest(fId)); + return this._fMediator.send(new GetDeepChildrenNodesAndGroupsRequest(fId)); } private getAllOutputIds(items: INodeWithDistanceRestrictions[]): string[] { @@ -79,7 +82,7 @@ export class CreateMoveNodesDragModelFromSelectionExecution } private getOutputsForNode(node: FNodeBase): FConnectorBase[] { - return this.fComponentsStore.fOutputs.filter((x) => node.isContains(x.hostElement)); + return this._fComponentsStore.fOutputs.filter((x) => node.isContains(x.hostElement)); } private getAllInputIds(items: INodeWithDistanceRestrictions[]): string[] { @@ -87,7 +90,7 @@ export class CreateMoveNodesDragModelFromSelectionExecution } private getInputsForNode(node: FNodeBase): FConnectorBase[] { - return this.fComponentsStore.fInputs.filter((x) => node.isContains(x.hostElement)); + return this._fComponentsStore.fInputs.filter((x) => node.isContains(x.hostElement)); } private getDragHandlersFromNodes(items: INodeWithDistanceRestrictions[]): IDraggableItem[] { @@ -95,8 +98,8 @@ export class CreateMoveNodesDragModelFromSelectionExecution items.forEach((node) => { result.push( - new NodeDragHandler(this.fDraggableDataContext, node.node, node.min, node.max), - ...(node.parentNodes || []).map(() => new NodeResizeByChildDragHandler(this.fDraggableDataContext)) + new NodeDragHandler(this._fDraggableDataContext, this._fComponentsStore, node.node, node.min, node.max), + ...(node.parentNodes || []).map(() => new NodeResizeByChildDragHandler(this._fDraggableDataContext)) ); }); return result; @@ -107,8 +110,8 @@ export class CreateMoveNodesDragModelFromSelectionExecution ): IDraggableItem[] { let result: IDraggableItem[] = handlers; handlers.filter((x) => x instanceof NodeDragHandler).forEach((dragHandler) => { - this.fMediator.send(new PutOutputConnectionHandlersToArrayRequest(dragHandler as NodeDragHandler, inputIds, result)); - this.fMediator.send(new PutInputConnectionHandlersToArrayRequest(dragHandler as NodeDragHandler, outputIds, result)); + this._fMediator.send(new PutOutputConnectionHandlersToArrayRequest(dragHandler as NodeDragHandler, inputIds, result)); + this._fMediator.send(new PutInputConnectionHandlersToArrayRequest(dragHandler as NodeDragHandler, outputIds, result)); }); return result; } diff --git a/projects/f-flow/src/f-draggable/node/create-move-nodes-drag-model-from-selection/domain/get-node-move-restrictions/get-node-move-restrictions.execution.ts b/projects/f-flow/src/f-draggable/node/create-move-nodes-drag-model-from-selection/domain/get-node-move-restrictions/get-node-move-restrictions.execution.ts index 1849476..6950494 100644 --- a/projects/f-flow/src/f-draggable/node/create-move-nodes-drag-model-from-selection/domain/get-node-move-restrictions/get-node-move-restrictions.execution.ts +++ b/projects/f-flow/src/f-draggable/node/create-move-nodes-drag-model-from-selection/domain/get-node-move-restrictions/get-node-move-restrictions.execution.ts @@ -1,25 +1,23 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { GetNodeMoveRestrictionsRequest } from './get-node-move-restrictions.request'; import { IRect, PointExtensions } from '@foblex/2d'; import { INodeMoveRestrictions } from './i-node-move-restrictions'; import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; -import { GetNormalizedNodeRectRequest, GetNormalizedParentNodeRectRequest } from '../../../../domain'; +import { GetNormalizedParentNodeRectRequest } from '../../../../domain'; import { FNodeBase } from '../../../../../f-node'; +import { GetNormalizedElementRectRequest } from '../../../../../domain'; @Injectable() @FExecutionRegister(GetNodeMoveRestrictionsRequest) export class GetNodeMoveRestrictionsExecution implements IExecution { - constructor( - private fMediator: FMediator - ) { - } + private _fMediator = inject(FMediator); public handle(request: GetNodeMoveRestrictionsRequest): INodeMoveRestrictions { if (request.fNode.fParentId && !request.hasParentNodeInSelected) { const fParentNodeRect = this.getParentNodeRect(request.fNode); - const fCurrentNodeRect = this.getCurrentNodeRect(request.fNode); + const fCurrentNodeRect = this._getNodeRect(request.fNode); return { min: PointExtensions.initialize(fParentNodeRect.x - fCurrentNodeRect.x, fParentNodeRect.y - fCurrentNodeRect.y), max: PointExtensions.initialize( @@ -31,12 +29,12 @@ export class GetNodeMoveRestrictionsExecution return { ...DEFAULT_RESTRICTIONS }; } - private getCurrentNodeRect(fNode: FNodeBase): IRect { - return this.fMediator.send(new GetNormalizedNodeRectRequest(fNode)); + private _getNodeRect(fNode: FNodeBase): IRect { + return this._fMediator.send(new GetNormalizedElementRectRequest(fNode.hostElement)); } private getParentNodeRect(fNode: FNodeBase): IRect { - return this.fMediator.send(new GetNormalizedParentNodeRectRequest(fNode)); + return this._fMediator.send(new GetNormalizedParentNodeRectRequest(fNode)); } } diff --git a/projects/f-flow/src/f-draggable/node/node-move-finalize/node-move-finalize.execution.ts b/projects/f-flow/src/f-draggable/node/node-move-finalize/node-move-finalize.execution.ts index f1e1376..4a9d34d 100644 --- a/projects/f-flow/src/f-draggable/node/node-move-finalize/node-move-finalize.execution.ts +++ b/projects/f-flow/src/f-draggable/node/node-move-finalize/node-move-finalize.execution.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { NodeMoveFinalizeRequest } from './node-move-finalize.request'; import { IPoint, Point } from '@foblex/2d'; import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; @@ -6,50 +6,70 @@ import { FComponentsStore } from '../../../f-storage'; import { FDraggableDataContext } from '../../f-draggable-data-context'; import { IsConnectionUnderNodeRequest -} from '../../domain/is-connection-under-node/is-connection-under-node.request'; +} from '../../domain'; import { IDraggableItem } from '../../i-draggable-item'; import { NodeDragToParentDragHandler } from '../node-drag-to-parent.drag-handler'; +import { ILineAlignmentResult } from '../../../f-line-alignment'; +import { NodeDragHandler } from '../node.drag-handler'; @Injectable() @FExecutionRegister(NodeMoveFinalizeRequest) export class NodeMoveFinalizeExecution implements IExecution { - private get flowHost(): HTMLElement { - return this.fComponentsStore.fFlow!.hostElement; - } + private _fMediator = inject(FMediator); + private _fComponentsStore = inject(FComponentsStore); + private _fDraggableDataContext = inject(FDraggableDataContext); - constructor( - private fComponentsStore: FComponentsStore, - private fDraggableDataContext: FDraggableDataContext, - private fMediator: FMediator - ) { + private get _fHost(): HTMLElement { + return this._fComponentsStore.fFlow!.hostElement; } public handle(request: NodeMoveFinalizeRequest): void { - const difference = this.getDifferenceWithLineAlignment( - this.getDifferenceBetweenPreparationAndFinalize(request.event.getPosition()) + const difference = this._getDifferenceWithLineAlignment( + this._getDifferenceBetweenPreparationAndFinalize(request.event.getPosition()) ); - this.getItems().forEach((x) => { + + const firstNodeOrGroup = this._fDraggableDataContext.draggableItems + .find((x) => x instanceof NodeDragHandler)!; + + const differenceWithCellSize = firstNodeOrGroup.getDifferenceWithCellSize(difference); + + this._finalizeMove(differenceWithCellSize); + + this._fMediator.send(new IsConnectionUnderNodeRequest()); + this._fDraggableDataContext.fLineAlignment?.complete(); + } + + private _finalizeMove(difference: IPoint): void { + this._getItems().forEach((x) => { x.onPointerMove({ ...difference }); x.onPointerUp?.(); }); - this.fMediator.send(new IsConnectionUnderNodeRequest()); - this.fDraggableDataContext.fLineAlignment?.complete(); } - private getItems(): IDraggableItem[] { - return this.fDraggableDataContext.draggableItems + private _getItems(): IDraggableItem[] { + return this._fDraggableDataContext.draggableItems .filter((x) => !(x instanceof NodeDragToParentDragHandler)); } - private getDifferenceBetweenPreparationAndFinalize(position: IPoint): Point { - return Point.fromPoint(position).elementTransform(this.flowHost) - .div(this.fDraggableDataContext.onPointerDownScale) - .sub(this.fDraggableDataContext.onPointerDownPosition); + private _getDifferenceBetweenPreparationAndFinalize(position: IPoint): Point { + return Point.fromPoint(position).elementTransform(this._fHost) + .div(this._fDraggableDataContext.onPointerDownScale) + .sub(this._fDraggableDataContext.onPointerDownPosition); + } + + private _getDifferenceWithLineAlignment(difference: IPoint): IPoint { + return this._applyLineAlignmentDifference( + difference, + this._getLineAlignmentDifference(difference) + ); + } + + private _getLineAlignmentDifference(difference: IPoint): ILineAlignmentResult | undefined { + return this._fDraggableDataContext.fLineAlignment?.findNearestCoordinate(difference); } - private getDifferenceWithLineAlignment(difference: IPoint): IPoint { - const intersection = this.fDraggableDataContext.fLineAlignment?.findNearestCoordinate(difference); + private _applyLineAlignmentDifference(difference: IPoint, intersection: ILineAlignmentResult | undefined): IPoint { if (intersection) { difference.x = intersection.xResult.value ? (difference.x - intersection.xResult.distance!) : difference.x; difference.y = intersection.yResult.value ? (difference.y - intersection.yResult.distance!) : difference.y; diff --git a/projects/f-flow/src/f-draggable/node/node-move-preparation/node-move-preparation.execution.ts b/projects/f-flow/src/f-draggable/node/node-move-preparation/node-move-preparation.execution.ts index 0f4e59c..a8f6fd1 100644 --- a/projects/f-flow/src/f-draggable/node/node-move-preparation/node-move-preparation.execution.ts +++ b/projects/f-flow/src/f-draggable/node/node-move-preparation/node-move-preparation.execution.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { NodeMovePreparationRequest } from './node-move-preparation.request'; import { ITransformModel, Point } from '@foblex/2d'; import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; @@ -14,66 +14,68 @@ import { SelectAndUpdateNodeLayerRequest } from '../../../domain'; @FExecutionRegister(NodeMovePreparationRequest) export class NodeMovePreparationExecution implements IExecution { - private get transform(): ITransformModel { - return this.fComponentsStore.fCanvas!.transform; - } + private _fMediator = inject(FMediator); + private _fComponentsStore = inject(FComponentsStore); + private _fDraggableDataContext = inject(FDraggableDataContext); - private get flowHost(): HTMLElement { - return this.fComponentsStore.fFlow!.hostElement; + private get _transform(): ITransformModel { + return this._fComponentsStore.fCanvas!.transform; } - constructor( - private fComponentsStore: FComponentsStore, - private fDraggableDataContext: FDraggableDataContext, - private fMediator: FMediator - ) { + private get _fHost(): HTMLElement { + return this._fComponentsStore.fFlow!.hostElement; } public handle(request: NodeMovePreparationRequest): void { - const node = this.getNode(request.event.targetElement); - if (!node) { - throw new Error('Node not found'); - } - let itemsToDrag: IDraggableItem[] = []; - if (!node.fSelectionDisabled) { - this.selectAndUpdateNodeLayer(node); - itemsToDrag = this.createDragModelFromSelection(); - } else { - itemsToDrag = this.createDragModelFromSelection(node); - } - this.initializeLineAlignment(this.filterNodesFromDraggableItems(itemsToDrag)); + const itemsToDrag = this._calculateDraggableItems( + this._getNode(request.event.targetElement) + ); + + this._initializeLineAlignment(itemsToDrag); - this.fDraggableDataContext.onPointerDownScale = this.transform.scale; - this.fDraggableDataContext.onPointerDownPosition = Point.fromPoint(request.event.getPosition()) - .elementTransform(this.flowHost).div(this.transform.scale); - this.fDraggableDataContext.draggableItems = itemsToDrag; + this._fDraggableDataContext.onPointerDownScale = this._transform.scale; + this._fDraggableDataContext.onPointerDownPosition = Point.fromPoint(request.event.getPosition()) + .elementTransform(this._fHost).div(this._transform.scale); + this._fDraggableDataContext.draggableItems = itemsToDrag; } - private selectAndUpdateNodeLayer(node: FNodeBase) { - this.fMediator.send( - new SelectAndUpdateNodeLayerRequest(node) - ); + private _getNode(targetElement: HTMLElement): FNodeBase { + const result = this._fComponentsStore.fNodes.find(n => n.isContains(targetElement))!; + if (!result) { + throw new Error('Node not found'); + } + return result; } - private getNode(targetElement: HTMLElement): FNodeBase { - return this.fComponentsStore.fNodes.find(n => n.isContains(targetElement))!; + //We drag nodes from selection model + private _calculateDraggableItems(fNode: FNodeBase): IDraggableItem[] { + let result: IDraggableItem[] = []; + if (!fNode.fSelectionDisabled) { + // Need to select node before drag + this._fMediator.send(new SelectAndUpdateNodeLayerRequest(fNode)); + + result = this._dragModelFromSelection(); + } else { + // User can drag node that can't be selected + result = this._dragModelFromSelection(fNode); + } + return result; } - private createDragModelFromSelection(nodeWithDisabledSelection?: FNodeBase): IDraggableItem[] { - return this.fMediator.send( + private _dragModelFromSelection(nodeWithDisabledSelection?: FNodeBase): IDraggableItem[] { + return this._fMediator.send( new CreateMoveNodesDragModelFromSelectionRequest(nodeWithDisabledSelection) ); } - private initializeLineAlignment(nodesToDrag: FNodeBase[]): void { - this.fDraggableDataContext.fLineAlignment?.initialize( - this.fComponentsStore.fNodes, - nodesToDrag + private _initializeLineAlignment(itemsToDrag: IDraggableItem[]): void { + this._fDraggableDataContext.fLineAlignment?.initialize( + this._fComponentsStore.fNodes, this._filterNodesFromDraggableItems(itemsToDrag) ); } - private filterNodesFromDraggableItems(items: IDraggableItem[]): FNodeBase[] { + private _filterNodesFromDraggableItems(items: IDraggableItem[]): FNodeBase[] { return items.filter((x) => x instanceof NodeDragHandler) .map(x => (x as NodeDragHandler).fNode); } diff --git a/projects/f-flow/src/f-draggable/node/node-move-preparation/node-move-preparation.validator.ts b/projects/f-flow/src/f-draggable/node/node-move-preparation/node-move-preparation.validator.ts index f92941f..31a4a33 100644 --- a/projects/f-flow/src/f-draggable/node/node-move-preparation/node-move-preparation.validator.ts +++ b/projects/f-flow/src/f-draggable/node/node-move-preparation/node-move-preparation.validator.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { NodeMovePreparationRequest } from './node-move-preparation.request'; import { FValidatorRegister, IValidator } from '@foblex/mediator'; import { FComponentsStore } from '../../../f-storage'; @@ -10,31 +10,28 @@ import { isClosestElementHasClass } from '@foblex/utils'; @FValidatorRegister(NodeMovePreparationRequest) export class NodeMovePreparationValidator implements IValidator { - constructor( - private fComponentsStore: FComponentsStore, - private fDraggableDataContext: FDraggableDataContext - ) { - } + private _fComponentsStore = inject(FComponentsStore); + private _fDraggableDataContext = inject(FDraggableDataContext); public handle(request: NodeMovePreparationRequest): boolean { - return this.isDragHandlesEmpty() - && this.isDragHandleElement(request.event.targetElement) - && this.isNodeCanBeDragged(this.getNode(request.event.targetElement)); + return this._isDragHandlesEmpty() + && this._isDragHandleElement(request.event.targetElement) + && this._isNodeCanBeDragged(this._getNode(request.event.targetElement)); } - private isDragHandlesEmpty(): boolean { - return !this.fDraggableDataContext.draggableItems.length; + private _isDragHandlesEmpty(): boolean { + return !this._fDraggableDataContext.draggableItems.length; } - private getNode(targetElement: HTMLElement): FNodeBase | undefined { - return this.fComponentsStore.fNodes.find(n => n.isContains(targetElement)); + private _isDragHandleElement(element: HTMLElement): boolean { + return isClosestElementHasClass(element, '.f-drag-handle'); } - private isNodeCanBeDragged(node: FNodeBase | undefined): boolean { - return !!node && !node.fDraggingDisabled; + private _isNodeCanBeDragged(fNode: FNodeBase | undefined): boolean { + return !!fNode && !fNode.fDraggingDisabled; } - private isDragHandleElement(targetElement: HTMLElement): boolean { - return isClosestElementHasClass(targetElement, '.f-drag-handle'); + private _getNode(element: HTMLElement): FNodeBase | undefined { + return this._fComponentsStore.fNodes.find(x => x.isContains(element)); } } diff --git a/projects/f-flow/src/f-draggable/node/node.drag-handler.ts b/projects/f-flow/src/f-draggable/node/node.drag-handler.ts index a1dd66e..b18de13 100644 --- a/projects/f-flow/src/f-draggable/node/node.drag-handler.ts +++ b/projects/f-flow/src/f-draggable/node/node.drag-handler.ts @@ -5,39 +5,41 @@ import { FNodeBase } from '../../f-node'; import { INodeMoveRestrictions } from './create-move-nodes-drag-model-from-selection'; +import { FComponentsStore } from '../../f-storage'; export class NodeDragHandler implements IDraggableItem { - private onPointerDownPosition: IPoint = PointExtensions.initialize(); + private readonly _onPointerDownPosition = PointExtensions.initialize(); constructor( private fDraggableDataContext: FDraggableDataContext, + private fComponentsStore: FComponentsStore, public fNode: FNodeBase, public minDistance: IPoint, public maxDistance: IPoint, ) { - this.onPointerDownPosition = { ...fNode.position }; + this._onPointerDownPosition = { ...fNode.position }; } public onPointerMove(difference: IPoint): void { - const restrictedDifference = this.getDifference(difference, { min: this.minDistance, max: this.maxDistance }); + const restrictedDifference = this._getDifference(difference, { min: this.minDistance, max: this.maxDistance }); - this.redrawNode(this.getNewPosition(restrictedDifference)); + this._redraw(this._getPosition(restrictedDifference)); this.fDraggableDataContext.fLineAlignment?.handle(restrictedDifference); } - private getNewPosition(difference: IPoint): IPoint { - return Point.fromPoint(this.onPointerDownPosition).add(difference); + private _getPosition(difference: IPoint): IPoint { + return Point.fromPoint(this._onPointerDownPosition).add(difference); } - private getDifference(difference: IPoint, restrictions: INodeMoveRestrictions): IPoint { + private _getDifference(difference: IPoint, restrictions: INodeMoveRestrictions): IPoint { return { x: Math.min(Math.max(difference.x, restrictions.min.x), restrictions.max.x), y: Math.min(Math.max(difference.y, restrictions.min.y), restrictions.max.y) } } - private redrawNode(position: IPoint): void { + private _redraw(position: IPoint): void { this.fNode.updatePosition(position); this.fNode.redraw(); } @@ -45,4 +47,20 @@ export class NodeDragHandler implements IDraggableItem { public onPointerUp(): void { this.fNode.positionChange.emit(this.fNode.position); } + + public getDifferenceWithCellSize(difference: IPoint): IPoint { + const restrictedDifference = this._getDifference(difference, { min: this.minDistance, max: this.maxDistance }); + const position = this._getPosition(restrictedDifference); + + return Point.fromPoint(this._applyCellSize(position)).sub(this._onPointerDownPosition); + } + + private _applyCellSize(position: IPoint): IPoint { + const hCellSize = this.fComponentsStore.fDraggable!.hCellSize; + const vCellSize = this.fComponentsStore.fDraggable!.vCellSize; + return { + x: Math.round(position.x / hCellSize) * hCellSize, + y: Math.round(position.y / vCellSize) * vCellSize + }; + } } diff --git a/projects/f-flow/src/f-node/domain/index.ts b/projects/f-flow/src/f-node/domain/index.ts deleted file mode 100644 index 1760961..0000000 --- a/projects/f-flow/src/f-node/domain/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './is-node'; diff --git a/projects/f-flow/src/f-node/f-drag-handle/f-drag-handle.directive.ts b/projects/f-flow/src/f-node/f-drag-handle.directive.ts similarity index 90% rename from projects/f-flow/src/f-node/f-drag-handle/f-drag-handle.directive.ts rename to projects/f-flow/src/f-node/f-drag-handle.directive.ts index 59f1afe..5ee63bc 100644 --- a/projects/f-flow/src/f-node/f-drag-handle/f-drag-handle.directive.ts +++ b/projects/f-flow/src/f-node/f-drag-handle.directive.ts @@ -1,7 +1,7 @@ import { Directive, ElementRef, inject, InjectionToken } from "@angular/core"; -import { IHasHostElement } from '../../i-has-host-element'; +import { IHasHostElement } from '../i-has-host-element'; export const F_DRAG_HANDLE = new InjectionToken('F_DRAG_HANDLE'); diff --git a/projects/f-flow/src/f-node/f-drag-handle/index.ts b/projects/f-flow/src/f-node/f-drag-handle/index.ts deleted file mode 100644 index fc5f5d6..0000000 --- a/projects/f-flow/src/f-node/f-drag-handle/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './f-drag-handle.directive'; diff --git a/projects/f-flow/src/f-node/index.ts b/projects/f-flow/src/f-node/index.ts index 2ebbf99..be9c61f 100644 --- a/projects/f-flow/src/f-node/index.ts +++ b/projects/f-flow/src/f-node/index.ts @@ -1,17 +1,17 @@ -export * from './domain'; - -export * from './f-drag-handle'; - export * from './f-resize-handle'; export * from './f-rotate-handle'; +export * from './f-drag-handle.directive'; + export * from './f-group.directive'; export * from './f-node.directive'; export * from './f-node-base'; +export * from './is-node'; + export * from './providers'; diff --git a/projects/f-flow/src/f-node/domain/is-node.ts b/projects/f-flow/src/f-node/is-node.ts similarity index 100% rename from projects/f-flow/src/f-node/domain/is-node.ts rename to projects/f-flow/src/f-node/is-node.ts diff --git a/projects/f-flow/src/f-node/providers.ts b/projects/f-flow/src/f-node/providers.ts index c571063..899ded7 100644 --- a/projects/f-flow/src/f-node/providers.ts +++ b/projects/f-flow/src/f-node/providers.ts @@ -1,7 +1,7 @@ import { FNodeDirective } from './f-node.directive'; -import { FDragHandleDirective } from './f-drag-handle'; import { FResizeHandleDirective } from './f-resize-handle'; import { FGroupDirective } from './f-group.directive'; +import { FDragHandleDirective } from './f-drag-handle.directive'; export const F_NODE_PROVIDERS = [ From b52cfc05dcaf2f6303d2dafc3c131b493cb4b2d5 Mon Sep 17 00:00:00 2001 From: Siarhei Huzarevich Date: Mon, 13 Jan 2025 21:04:09 +0100 Subject: [PATCH 2/2] docs: Added grid system example --- projects/f-flow/package.json | 2 +- public/markdown/examples/environment.ts | 18 +++++++++++++++ public/markdown/examples/grid-system.md | 21 ++++++++++++++++++ .../markdown/guides/f-draggable-directive.md | 6 ++++- .../examples/grid-system-example.dark.png | Bin 0 -> 25315 bytes .../examples/grid-system-example.light.png | Bin 0 -> 22822 bytes 6 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 public/markdown/examples/grid-system.md create mode 100644 public/previews/examples/grid-system-example.dark.png create mode 100644 public/previews/examples/grid-system-example.light.png diff --git a/projects/f-flow/package.json b/projects/f-flow/package.json index db354ec..a581f79 100644 --- a/projects/f-flow/package.json +++ b/projects/f-flow/package.json @@ -1,6 +1,6 @@ { "name": "@foblex/flow", - "version": "17.0.5", + "version": "17.0.6", "description": "An Angular library designed to simplify the creation and manipulation of dynamic flow. Provides components for flows, nodes, and connections, automating node manipulation and inter-node connections.", "main": "index.js", "types": "index.d.ts", diff --git a/public/markdown/examples/environment.ts b/public/markdown/examples/environment.ts index 9f394c4..44b0208 100644 --- a/public/markdown/examples/environment.ts +++ b/public/markdown/examples/environment.ts @@ -176,6 +176,10 @@ function createEnvironment(): IDocsEnvironment { tag: 'tournament-bracket', component: import('../../../projects/f-pro-examples/tournament-bracket/tournament-bracket.component') }, + { + tag: 'grid-system-example', + component: import('../../../projects/f-examples/extensions/grid-system-example/grid-system-example.component') + } ], socialLinks: [ { icon: 'github', link: 'https://github.com/Foblex/f-flow' }, @@ -545,6 +549,20 @@ function extensionGroup(): INavigationGroup { image_height: 600, image_type: 'image/png', }, + { + link: 'grid-system', + text: 'Grid System', + description: 'Add a grid system to Foblex Flow diagrams for Angular.', + image: './previews/examples/grid-system-example.light.png', + image_dark: './previews/examples/grid-system-example.dark.png', + image_width: 821, + image_height: 600, + image_type: 'image/png', + badge: { + text: 'New', + type: 'info' + } + }, { link: 'minimap', text: 'Minimap', diff --git a/public/markdown/examples/grid-system.md b/public/markdown/examples/grid-system.md new file mode 100644 index 0000000..a72ddcd --- /dev/null +++ b/public/markdown/examples/grid-system.md @@ -0,0 +1,21 @@ +# Grid System + +## Description + +This guide demonstrates how to position nodes in a grid system using Foblex Flow for Angular. To enable the grid system, you need to parameterize the [fDraggable](./docs/f-draggable-directive) directive with the grid properties `[vCellSize]` and `[hCellSize]`. + +- `[vCellSize]`: Defines the vertical size of each grid cell. +- `[hCellSize]`: Defines the horizontal size of each grid cell. + +## Example + +::: ng-component [height]="600" +[component.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/extensions/grid-system-example/grid-system-example.component.html +[component.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/extensions/grid-system-example/grid-system-example.component.ts +[component.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/extensions/grid-system-example/grid-system-example.component.scss +[common.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/_flow-common.scss +::: + + + + diff --git a/public/markdown/guides/f-draggable-directive.md b/public/markdown/guides/f-draggable-directive.md index 9ef3cd4..5a56a7d 100644 --- a/public/markdown/guides/f-draggable-directive.md +++ b/public/markdown/guides/f-draggable-directive.md @@ -6,7 +6,11 @@ The **FDraggableDirective** enhances a component, typically a flow [f-flow](f-fl ## Inputs - - `fDraggableDisabled: boolean;` Determines whether the draggable functionality is disabled. Default: `false + - `fDraggableDisabled: boolean;` Determines whether the draggable functionality is disabled. Default: `false`. + + - `vCellSize: number;` Defines the vertical size of each grid cell. Default: `1`. + + - `hCellSize: number;` Defines the horizontal size of each grid cell. Default: `1`. ## Outputs diff --git a/public/previews/examples/grid-system-example.dark.png b/public/previews/examples/grid-system-example.dark.png new file mode 100644 index 0000000000000000000000000000000000000000..550f760621710d145a2e8434ac7725f70a5303cf GIT binary patch literal 25315 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 Y+ss$?e3JzJYK6ic%Y7E(^b literal 0 HcmV?d00001 diff --git a/public/previews/examples/grid-system-example.light.png b/public/previews/examples/grid-system-example.light.png new file mode 100644 index 0000000000000000000000000000000000000000..6923e494934b28e2945df63ef19b2d7156a901a2 GIT binary patch literal 22822 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