@foblex/flow 19.1.4 → 19.1.6
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/AI.md +14 -8
- package/README.md +17 -4
- package/fesm2022/foblex-flow.mjs +87 -17
- package/fesm2022/foblex-flow.mjs.map +1 -1
- package/index.d.ts +34 -13
- package/package.json +1 -1
- package/schematics/ng-add/index.js +4 -2
- package/schematics/ng-add/index.js.map +1 -1
package/AI.md
CHANGED
|
@@ -5,12 +5,13 @@ Use this file as a strict control layer for code generation. Prefer verified pac
|
|
|
5
5
|
## What This Library Is
|
|
6
6
|
|
|
7
7
|
`@foblex/flow` is an Angular-native library for building node-based editors, workflow builders, and interactive graph UIs.
|
|
8
|
-
It provides rendering, connectors, interactions, selection, zoom, and connection drawing. By default your app owns
|
|
8
|
+
It provides rendering, connectors, interactions, selection, zoom, and connection drawing. By default your app owns graph records; the optional `withFlowState()` feature maintains typed records and undo/redo that your app explicitly loads and renders.
|
|
9
9
|
|
|
10
10
|
## Core Mental Model
|
|
11
11
|
|
|
12
|
-
- **Classic mode (default):** your app owns nodes, groups,
|
|
13
|
-
- **Managed mode (opt-in):** `provideFFlow(withFlowState())` provides `FFlowState`; supported completed gestures update its
|
|
12
|
+
- **Classic mode (default):** your app owns nodes, groups, and connections. User actions emit events; your handlers validate them, update app state, and Angular rerenders.
|
|
13
|
+
- **Managed mode (opt-in):** `provideFFlow(withFlowState())` provides `FFlowState`; supported completed gestures update its typed records and history automatically.
|
|
14
|
+
- **Application responsibility in both modes:** your app defines domain fields and owns validation policy, permissions, persistence, backend integration, and business meaning.
|
|
14
15
|
- Both modes render records through normal Angular templates. `withFlowState()` is optional and does not change classic event behavior when absent.
|
|
15
16
|
|
|
16
17
|
## Minimal Working Setup
|
|
@@ -136,11 +137,12 @@ export class Editor {
|
|
|
136
137
|
|
|
137
138
|
- Bind `state.nodes()`, `state.groups()` and `state.connections()` with `@for`; bind canvas `[position]` and `[scale]` to `state.transform()` when viewport undo/redo is enabled.
|
|
138
139
|
- Supported v1 gestures: create/reassign connection, move nodes/groups, delete selection, external-item creation, optional drop-to-group, selection, and canvas pan/zoom.
|
|
139
|
-
-
|
|
140
|
+
- Node and group geometry emitted through `fNodeSizeChange` / `fGroupSizeChange`, including user resize and auto-expand or auto-fit updates, changes the managed records without creating a separate history step. Arbitrary content measurement that emits neither output is not written to the store. Rotation and connection waypoint editing are not captured by managed state in v1.
|
|
140
141
|
- `state.changes()` increments once when a standalone mutation or outer batch settles. A drag can emit selection at start and move/drop at end while remaining one history step and one `changes()` increment.
|
|
141
|
-
- Use `state.snapshot()` for persistence; `load()`
|
|
142
|
+
- Use `state.snapshot()` for persistence; it includes graph records, selection, and viewport transform. `load()` restores them and resets history. Snapshots created before selection persistence remain valid and load with an empty selection.
|
|
142
143
|
- `canvasTransformDebounce` defaults to `350ms`. It waits for wheel/pinch zoom events to settle, then records the current canvas transform as one change and one undo item. Setting it to `0` disables batching: every emitted `fCanvasChange` is applied immediately, increments `state.changes()`, and normally creates a separate undo item. Keep the canvas `[debounceTime]` unset when managed state owns viewport history; do not stack canvas and State debounce layers.
|
|
143
144
|
- For initial or other application-driven viewport positioning, suppress the event at the canvas helper: `canvas.resetScaleAndCenter(false, false)`. The second `false` means `emitCanvasChange = false`, so managed history is untouched.
|
|
145
|
+
- `canvas.centerGroupOrNode(id)` preserves the current zoom. Use `canvas.resetScaleAndCenterGroupOrNode(id)` when focusing the item must also reset zoom to `1`; both methods emit `fCanvasChange` by default.
|
|
144
146
|
- Connection endpoints are connector ids. Automatic cascade from node/group deletion uses the rendered connector registry; before connectors render, remove known attached connection ids explicitly in the same `state.batch(...)`.
|
|
145
147
|
|
|
146
148
|
### Async reflow must stay inside an open transaction
|
|
@@ -180,7 +182,7 @@ When the flow compiles but looks wrong, verify in this order:
|
|
|
180
182
|
10. **Connections attach to the wrong place** (`FF1006`): a connector is hidden with CSS (`display: none`) — its geometry is a 0×0 point. Conditionally render instead of hiding.
|
|
181
183
|
11. **Node moves but its bindings never fire** (`FF1007`): an `fNode` element is nested inside another node element. One `fNode` per node; hierarchy is id-based (`fNodeParentId`), not DOM-based.
|
|
182
184
|
12. **Group behaviors don't apply** (`FF1008`): `fNodeParentId` / `fGroupParentId` references an id no rendered group has.
|
|
183
|
-
13. **Wrong initial viewport** (`FF1009`): `fitToScreen()` / `resetScaleAndCenter()` / `centerGroupOrNode()` called before nodes were rendered — call them from `(fNodesRendered)` (earliest safe) or `(fFullRendered)`.
|
|
185
|
+
13. **Wrong initial viewport** (`FF1009`): `fitToScreen()` / `resetScaleAndCenter()` / `centerGroupOrNode()` / `resetScaleAndCenterGroupOrNode()` called before nodes were rendered — call them from `(fNodesRendered)` (earliest safe) or `(fFullRendered)`.
|
|
184
186
|
14. **Initial centering appears in managed undo history**: call `resetScaleAndCenter(false, false)` (or pass `emitCanvasChange: false` to another viewport helper) for an application-driven transform.
|
|
185
187
|
|
|
186
188
|
To verify programmatically: listen to `(fFullRendered)` on `<f-flow>`, then call `flow.getState()` and assert every declared connection resolved to existing connectors.
|
|
@@ -192,9 +194,9 @@ To verify programmatically: listen to `(fFullRendered)` on `<f-flow>`, then call
|
|
|
192
194
|
- Style flow internals (connection paths, minimap, markers) in global styles or via `::ng-deep` — component-scoped CSS never reaches them. Wire the default theme via `ng add`.
|
|
193
195
|
- Do not combine `fAutoSizeToFitChildren` with restoring a persisted group size in the same render: pass `false` while restoring, enable it afterwards.
|
|
194
196
|
- With several `<f-flow>` instances on one page, keep `fDraggable` enabled only on the active flow.
|
|
195
|
-
-
|
|
197
|
+
- For custom markers, either provide `SELECTED_START` / `SELECTED_END` alongside `START` / `END`, or use `START_ALL_STATES` / `END_ALL_STATES`; the all-states variants create both normal and selected markers.
|
|
196
198
|
- An empty `fCanBeConnectedTo` allow-list means "no restriction", not "allow nothing"; category strings must match exactly.
|
|
197
|
-
-
|
|
199
|
+
- Start with regular `@for` rendering and measure a production build. Enable `[fCache]` when geometry reads are a demonstrated cost; use `*fVirtualFor` inside `<ng-container ngProjectAs="[fNodes]">` when progressive creation materially improves startup. `fVirtualFor` is not viewport culling.
|
|
198
200
|
|
|
199
201
|
## Naming Distinction
|
|
200
202
|
|
|
@@ -217,7 +219,11 @@ See [STYLING.md](./STYLING.md).
|
|
|
217
219
|
- Human docs: https://flow.foblex.com/docs/get-started
|
|
218
220
|
- Live examples with source: https://flow.foblex.com/examples/overview
|
|
219
221
|
- Managed state example and contract: https://flow.foblex.com/examples/state
|
|
222
|
+
- Managed state guide: https://flow.foblex.com/docs/managed-flow-state
|
|
220
223
|
- Managed state + asynchronous reflow recipe: https://flow.foblex.com/examples/state
|
|
224
|
+
- Large-flow performance guide: https://flow.foblex.com/docs/large-flow-performance
|
|
225
|
+
- Shadow DOM and Angular Elements: https://flow.foblex.com/docs/shadow-dom-and-angular-elements
|
|
226
|
+
- Unified connector migration: https://flow.foblex.com/docs/migrating-to-unified-connectors
|
|
221
227
|
|
|
222
228
|
## Fallback Rule
|
|
223
229
|
|
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
Foblex Flow gives Angular teams a simple way to start building graph-based products without adopting a React-first mental model. Begin with `f-flow`, `f-canvas`, nodes, and connections, then add richer editor features only when your product needs them.
|
|
32
32
|
|
|
33
|
-
Use it to create workflow builders, AI low-code tools, call-flow editors, UML diagrams, internal back-office tools, and other node-based interfaces
|
|
33
|
+
Use it to create workflow builders, AI low-code tools, call-flow editors, UML diagrams, internal back-office tools, and other node-based interfaces. Start with app-owned graph records, or opt into managed records and undo/redo with `withFlowState()`; validation, persistence, permissions, and domain logic remain application concerns in both modes.
|
|
34
34
|
|
|
35
35
|
<p align="center">
|
|
36
36
|
<a href="https://flow.foblex.com/examples/overview">
|
|
@@ -47,7 +47,8 @@ Current `19.x` releases target Angular `17.3+`. If your app is on Angular 12-17.
|
|
|
47
47
|
- Built for real editor interactions: drag to connect, drag to reassign, selection, zoom, minimap, snapping, alignment helpers, and waypoints.
|
|
48
48
|
- Advanced modules are optional: caching and virtualization are scaling tools, not day-one requirements.
|
|
49
49
|
- Custom nodes, connectors, and connections for domain-specific graph UIs.
|
|
50
|
-
-
|
|
50
|
+
- Choose the state boundary that fits your editor: app-owned records by default, or optional typed `FFlowState` records with snapshots and undo/redo.
|
|
51
|
+
- Your app stays in control of validation rules, permissions, persistence, and business meaning in either state mode.
|
|
51
52
|
- Suitable for both lightweight diagrams and full workflow-builder products.
|
|
52
53
|
|
|
53
54
|
## Feature Overview
|
|
@@ -61,11 +62,19 @@ Current `19.x` releases target Angular `17.3+`. If your app is on Angular 12-17.
|
|
|
61
62
|
| Layout | Dagre and ELK auto-layout packages, reflow-on-resize, layer ordering |
|
|
62
63
|
| Scale | Node virtualization, render caching, background workers — optional, for large scenes |
|
|
63
64
|
| Customization | Fully templated nodes/connections/connectors, themable via CSS tokens/SCSS mixins, custom markers, connection gradients and labels |
|
|
64
|
-
| Integration |
|
|
65
|
+
| Integration | Classic event-driven API with app-owned records, or optional `withFlowState()` managed records, snapshots, and undo/redo; SSR-safe, zoneless-ready |
|
|
65
66
|
| AI tooling | `llms.txt`, bundled `AI.md`, `ng add` writes agent rules, dev diagnostics with stable `FFxxxx` error codes |
|
|
66
67
|
|
|
67
68
|
Coming from React Flow? Read the honest comparison: [React Flow vs Foblex Flow for Angular teams](https://flow.foblex.com/docs/react-flow-vs-foblex-flow-for-angular-teams).
|
|
68
69
|
|
|
70
|
+
## Choose Your State Integration
|
|
71
|
+
|
|
72
|
+
The default **classic mode** renders arrays or signals owned by your application. Foblex Flow emits final interaction events such as `fCreateConnection` and `fMoveNodes`; your handlers validate them and update application state.
|
|
73
|
+
|
|
74
|
+
The optional **managed mode** installs `provideFFlow(withFlowState())`. Its typed `FFlowState` records apply supported gestures automatically and provide batching, snapshots, undo, and redo. Your application still defines domain fields and decides how records are validated and persisted.
|
|
75
|
+
|
|
76
|
+
[Managed Flow State guide](https://flow.foblex.com/docs/managed-flow-state) · [Live state example](https://flow.foblex.com/examples/state)
|
|
77
|
+
|
|
69
78
|
## What You Can Build
|
|
70
79
|
|
|
71
80
|
- Angular node editors
|
|
@@ -157,7 +166,7 @@ Full guide: [Default Theme and Styling](https://flow.foblex.com/docs/default-the
|
|
|
157
166
|
</f-flow>
|
|
158
167
|
```
|
|
159
168
|
|
|
160
|
-
That is the
|
|
169
|
+
That is the rendering mental model: `f-flow` hosts the editor, `f-canvas` pans and zooms, any element becomes a node with `fNode`, and `fConnector` endpoints attach connections. State management and every advanced feature remain explicit choices.
|
|
161
170
|
|
|
162
171
|
## Quick FAQ
|
|
163
172
|
|
|
@@ -169,6 +178,10 @@ That is the whole mental model: `f-flow` hosts the editor, `f-canvas` pans and z
|
|
|
169
178
|
|
|
170
179
|
- [Get Started](https://flow.foblex.com/docs/get-started)
|
|
171
180
|
- [Angular Version Compatibility](https://flow.foblex.com/docs/angular-version-compatibility)
|
|
181
|
+
- [Managed Flow State](https://flow.foblex.com/docs/managed-flow-state)
|
|
182
|
+
- [Large Flow Performance](https://flow.foblex.com/docs/large-flow-performance)
|
|
183
|
+
- [Shadow DOM and Angular Elements](https://flow.foblex.com/docs/shadow-dom-and-angular-elements)
|
|
184
|
+
- [Migrating to Unified Connectors](https://flow.foblex.com/docs/migrating-to-unified-connectors)
|
|
172
185
|
- [Documentation](https://flow.foblex.com/docs/intro)
|
|
173
186
|
- [Examples](https://flow.foblex.com/examples/overview)
|
|
174
187
|
- [Articles](https://flow.foblex.com/blog/overview)
|
package/fesm2022/foblex-flow.mjs
CHANGED
|
@@ -925,11 +925,13 @@ class CenterGroupOrNodeRequest {
|
|
|
925
925
|
id;
|
|
926
926
|
animated;
|
|
927
927
|
emitCanvasChange;
|
|
928
|
+
resetScale;
|
|
928
929
|
static fToken = Symbol('CenterGroupOrNodeRequest');
|
|
929
|
-
constructor(id, animated, emitCanvasChange = true) {
|
|
930
|
+
constructor(id, animated, emitCanvasChange = true, resetScale = false) {
|
|
930
931
|
this.id = id;
|
|
931
932
|
this.animated = animated;
|
|
932
933
|
this.emitCanvasChange = emitCanvasChange;
|
|
934
|
+
this.resetScale = resetScale;
|
|
933
935
|
}
|
|
934
936
|
}
|
|
935
937
|
|
|
@@ -942,17 +944,21 @@ let CenterGroupOrNode = class CenterGroupOrNode {
|
|
|
942
944
|
get _transform() {
|
|
943
945
|
return this._store.transform;
|
|
944
946
|
}
|
|
945
|
-
handle({ id, animated, emitCanvasChange }) {
|
|
947
|
+
handle({ id, animated, emitCanvasChange, resetScale }) {
|
|
946
948
|
const node = this._store.nodes.get(id);
|
|
947
949
|
if (!node) {
|
|
948
950
|
return;
|
|
949
951
|
}
|
|
950
|
-
this._toCenter(this._getNodeRect(node), this._getFlowRect(), node._position);
|
|
952
|
+
this._toCenter(this._getNodeRect(node), this._getFlowRect(), node._position, resetScale);
|
|
951
953
|
this._mediator.execute(new RedrawCanvasWithAnimationRequest(animated, ECanvasRedrawContext.VIEWPORT_ONLY, emitCanvasChange));
|
|
952
954
|
}
|
|
953
|
-
_toCenter(fNodeRect, fFlowRect, position) {
|
|
955
|
+
_toCenter(fNodeRect, fFlowRect, position, resetScale) {
|
|
956
|
+
const currentScale = this._transform.scale;
|
|
957
|
+
const targetScale = resetScale ? 1 : currentScale;
|
|
958
|
+
const sizeScale = currentScale === 0 ? 1 : targetScale / currentScale;
|
|
954
959
|
this._transform.scaledPosition = PointExtensions.initialize();
|
|
955
|
-
this._transform.
|
|
960
|
+
this._transform.scale = targetScale;
|
|
961
|
+
this._transform.position = PointExtensions.initialize((fFlowRect.width - fNodeRect.width * sizeScale) / 2 - position.x * targetScale, (fFlowRect.height - fNodeRect.height * sizeScale) / 2 - position.y * targetScale);
|
|
956
962
|
}
|
|
957
963
|
_getNodeRect(fNode) {
|
|
958
964
|
return RectExtensions.fromElement(fNode.hostElement);
|
|
@@ -21013,9 +21019,17 @@ class FCanvasComponent extends FCanvasBase {
|
|
|
21013
21019
|
}
|
|
21014
21020
|
centerGroupOrNode(groupOrNodeId, animated = true, emitCanvasChange = true) {
|
|
21015
21021
|
this._warnWhenCalledBeforeNodesRender('centerGroupOrNode()');
|
|
21016
|
-
this.
|
|
21017
|
-
|
|
21018
|
-
|
|
21022
|
+
this._centerGroupOrNode(groupOrNodeId, animated, emitCanvasChange, false);
|
|
21023
|
+
}
|
|
21024
|
+
/**
|
|
21025
|
+
* Resets the canvas scale to `1` and centers the specified group or node.
|
|
21026
|
+
* @param groupOrNodeId - The ID of the group or node to center.
|
|
21027
|
+
* @param animated - If true, the centering will be animated; otherwise, it will be instantaneous.
|
|
21028
|
+
* @param emitCanvasChange - If false, does not emit `fCanvasChange` for this programmatic move.
|
|
21029
|
+
*/
|
|
21030
|
+
resetScaleAndCenterGroupOrNode(groupOrNodeId, animated = true, emitCanvasChange = true) {
|
|
21031
|
+
this._warnWhenCalledBeforeNodesRender('resetScaleAndCenterGroupOrNode()');
|
|
21032
|
+
this._centerGroupOrNode(groupOrNodeId, animated, emitCanvasChange, true);
|
|
21019
21033
|
}
|
|
21020
21034
|
/**
|
|
21021
21035
|
* Fits the canvas to the screen by adjusting the scale and position.
|
|
@@ -21072,6 +21086,11 @@ class FCanvasComponent extends FCanvasBase {
|
|
|
21072
21086
|
_afterRedraw(callback) {
|
|
21073
21087
|
this._mediator.execute(new WaitForConnectionsRenderedRequest(this._components.connectionsRevision, this._components.nodesRevision, () => afterNextRender(callback, { injector: this._injector }), this.destroyRef));
|
|
21074
21088
|
}
|
|
21089
|
+
_centerGroupOrNode(groupOrNodeId, animated, emitCanvasChange, resetScale) {
|
|
21090
|
+
this._afterRedraw(() => {
|
|
21091
|
+
this._mediator.execute(new CenterGroupOrNodeRequest(groupOrNodeId, animated, emitCanvasChange, resetScale));
|
|
21092
|
+
});
|
|
21093
|
+
}
|
|
21075
21094
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: FCanvasComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
21076
21095
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: FCanvasComponent, isStandalone: true, selector: "f-canvas", inputs: { position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, scale: { classPropertyName: "scale", publicName: "scale", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null }, fLayers: { classPropertyName: "fLayers", publicName: "fLayers", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { fCanvasChange: "fCanvasChange" }, host: { classAttribute: "f-component f-canvas" }, providers: [{ provide: F_CANVAS, useExisting: FCanvasComponent }], viewQueries: [{ propertyName: "fGroupsContainer", first: true, predicate: ["fGroupsContainer"], descendants: true, isSignal: true }, { propertyName: "fNodesContainer", first: true, predicate: ["fNodesContainer"], descendants: true, isSignal: true }, { propertyName: "fConnectionsContainer", first: true, predicate: ["fConnectionsContainer"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<ng-container>\n @for (layer of resolvedLayers(); track layer) {\n @switch (layer) {\n @case ('groups') {\n <div #fGroupsContainer class=\"f-groups-container\">\n <ng-content select=\"[fGroup], [fGroups]\" />\n </div>\n }\n @case ('connections') {\n <div #fConnectionsContainer class=\"f-connections-container\">\n <ng-content select=\"f-snap-connection\" />\n <ng-content select=\"f-connection, [fConnections]\" />\n <ng-content select=\"f-connection-for-create\" />\n </div>\n }\n @case ('nodes') {\n <div #fNodesContainer class=\"f-nodes-container\">\n <ng-content select=\"[fNode], [fNodes]\" />\n </div>\n }\n }\n }\n</ng-container>\n", styles: [":host{position:absolute;overflow:visible;width:100%;height:100%;left:0;top:0;transform-origin:0 0;pointer-events:none}:host.f-canvas-dragging,:host.canvas-dragging{transform:translateZ(0)}.f-groups-container,.f-connections-container,.f-nodes-container{isolation:isolate}.f-connections-container{position:absolute}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
21077
21096
|
}
|
|
@@ -21780,6 +21799,7 @@ class FA11yController {
|
|
|
21780
21799
|
const direction = ARROW_DIRECTIONS[event.key];
|
|
21781
21800
|
if (direction) {
|
|
21782
21801
|
event.preventDefault();
|
|
21802
|
+
this._syncActiveItemFromSelection();
|
|
21783
21803
|
event.ctrlKey || event.metaKey
|
|
21784
21804
|
? this._walkConnections(direction, event.shiftKey)
|
|
21785
21805
|
: this._navigate(direction, event.shiftKey);
|
|
@@ -21855,6 +21875,49 @@ class FA11yController {
|
|
|
21855
21875
|
!!target.closest('[fLockedContext]'));
|
|
21856
21876
|
}
|
|
21857
21877
|
// ------------------------------------------------------------------ navigation
|
|
21878
|
+
/**
|
|
21879
|
+
* Selection can be restored from state or replaced by a pointer-driven render before
|
|
21880
|
+
* this layer receives a stable DOM target. In that case there is no active descendant,
|
|
21881
|
+
* so use the single selected item as the spatial-navigation anchor.
|
|
21882
|
+
*/
|
|
21883
|
+
_syncActiveItemFromSelection() {
|
|
21884
|
+
const selection = this._selection();
|
|
21885
|
+
const selectedNodeIds = [...selection.fNodeIds, ...selection.fGroupIds];
|
|
21886
|
+
if (selectedNodeIds.length + selection.fConnectionIds.length !== 1) {
|
|
21887
|
+
return;
|
|
21888
|
+
}
|
|
21889
|
+
const selectedNodeId = selectedNodeIds[0];
|
|
21890
|
+
if (selectedNodeId) {
|
|
21891
|
+
if (this._activeNodeId === selectedNodeId && this._activeNode()) {
|
|
21892
|
+
return;
|
|
21893
|
+
}
|
|
21894
|
+
const node = this._store.nodes.getAll().find((x) => x.fId() === selectedNodeId);
|
|
21895
|
+
if (!node) {
|
|
21896
|
+
return;
|
|
21897
|
+
}
|
|
21898
|
+
this._activeNodeId = selectedNodeId;
|
|
21899
|
+
this._activeConnectionId = undefined;
|
|
21900
|
+
this._walkTargetNodeId = undefined;
|
|
21901
|
+
this._lastMove = undefined;
|
|
21902
|
+
this._setActiveDescendant(node.hostElement);
|
|
21903
|
+
return;
|
|
21904
|
+
}
|
|
21905
|
+
const selectedConnectionId = selection.fConnectionIds[0];
|
|
21906
|
+
if (this._activeConnectionId === selectedConnectionId && this._activeConnection()) {
|
|
21907
|
+
return;
|
|
21908
|
+
}
|
|
21909
|
+
const connection = this._store.connections
|
|
21910
|
+
.getAll()
|
|
21911
|
+
.find((x) => x.fId() === selectedConnectionId);
|
|
21912
|
+
if (!connection) {
|
|
21913
|
+
return;
|
|
21914
|
+
}
|
|
21915
|
+
this._activeConnectionId = selectedConnectionId;
|
|
21916
|
+
this._activeNodeId = undefined;
|
|
21917
|
+
this._walkTargetNodeId = undefined;
|
|
21918
|
+
this._lastMove = undefined;
|
|
21919
|
+
this._setActiveDescendant(connection.hostElement);
|
|
21920
|
+
}
|
|
21858
21921
|
/**
|
|
21859
21922
|
* Plain arrows travel over nodes AND connections as equal stops — a connection is
|
|
21860
21923
|
* represented by its midpoint, so walking a chain reads node → connection → node.
|
|
@@ -22644,14 +22707,15 @@ class FFlowState {
|
|
|
22644
22707
|
this._undoStack.length = 0;
|
|
22645
22708
|
this._redoStack.length = 0;
|
|
22646
22709
|
this._batchDirty = false;
|
|
22710
|
+
const selection = _copySelection(data.selection ?? EMPTY_SELECTION);
|
|
22647
22711
|
const transform = _copyTransform(data.transform ?? DEFAULT_TRANSFORM);
|
|
22648
|
-
this._liveSelection.set(
|
|
22712
|
+
this._liveSelection.set(selection);
|
|
22649
22713
|
this._liveTransform.set(transform);
|
|
22650
22714
|
this._shape.set({
|
|
22651
22715
|
nodes: _byId(data.nodes ?? []),
|
|
22652
22716
|
groups: _byId(data.groups ?? []),
|
|
22653
22717
|
connections: _byId(data.connections ?? []),
|
|
22654
|
-
selection
|
|
22718
|
+
selection,
|
|
22655
22719
|
transform,
|
|
22656
22720
|
});
|
|
22657
22721
|
this._syncHistorySignals();
|
|
@@ -22667,6 +22731,7 @@ class FFlowState {
|
|
|
22667
22731
|
nodes: Object.values(nodes).map(_copyBox),
|
|
22668
22732
|
groups: Object.values(groups).map(_copyBox),
|
|
22669
22733
|
connections: Object.values(connections).map((connection) => ({ ...connection })),
|
|
22734
|
+
selection: _copySelection(this.selection()),
|
|
22670
22735
|
transform: _copyTransform(this.transform()),
|
|
22671
22736
|
};
|
|
22672
22737
|
}
|
|
@@ -23167,6 +23232,13 @@ function _copyBox(box) {
|
|
|
23167
23232
|
size: box.size ? { ...box.size } : undefined,
|
|
23168
23233
|
};
|
|
23169
23234
|
}
|
|
23235
|
+
function _copySelection(selection) {
|
|
23236
|
+
return {
|
|
23237
|
+
nodeIds: [...selection.nodeIds],
|
|
23238
|
+
groupIds: [...selection.groupIds],
|
|
23239
|
+
connectionIds: [...selection.connectionIds],
|
|
23240
|
+
};
|
|
23241
|
+
}
|
|
23170
23242
|
/** Clears `parentId` on records that pointed at a removed group. */
|
|
23171
23243
|
function _clearParent(records, removedGroups) {
|
|
23172
23244
|
if (!removedGroups.size) {
|
|
@@ -23262,9 +23334,7 @@ class FFlowStateController {
|
|
|
23262
23334
|
this._wireCanvasEvents();
|
|
23263
23335
|
this._wireSizeChanges();
|
|
23264
23336
|
}));
|
|
23265
|
-
|
|
23266
|
-
this._wireSelectionRestore();
|
|
23267
|
-
}
|
|
23337
|
+
this._wireSelectionRestore();
|
|
23268
23338
|
}
|
|
23269
23339
|
destroy() {
|
|
23270
23340
|
this._disposers.forEach((dispose) => dispose());
|
|
@@ -23472,9 +23542,9 @@ class FFlowStateController {
|
|
|
23472
23542
|
}
|
|
23473
23543
|
}
|
|
23474
23544
|
/**
|
|
23475
|
-
*
|
|
23476
|
-
*
|
|
23477
|
-
*
|
|
23545
|
+
* Pushes state selection into the flow after `load`, `undo` or `redo` so the
|
|
23546
|
+
* rendered highlight follows the persisted state. `isSelectedChanged: false`
|
|
23547
|
+
* keeps this from re-emitting a selection change.
|
|
23478
23548
|
*/
|
|
23479
23549
|
_wireSelectionRestore() {
|
|
23480
23550
|
const state = this._state;
|
|
@@ -23531,7 +23601,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
23531
23601
|
* connection, node moves, drops into groups, external-item drops, delete
|
|
23532
23602
|
* requests) are applied to the state automatically, each as one undoable
|
|
23533
23603
|
* step. `undo()`/`redo()` with `canUndo`/`canRedo` signals come built in;
|
|
23534
|
-
* `snapshot()` returns the graph
|
|
23604
|
+
* `snapshot()` returns the graph, selection and viewport for persistence.
|
|
23535
23605
|
*
|
|
23536
23606
|
* Every store behavior is overridable: subclass `FFlowState` (any CRUD
|
|
23537
23607
|
* method, any `apply*` gesture handler, any protected building block) and
|