@statelyai/flow-dom 0.1.0 → 3.0.1
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/dist/esm/index.d.ts +120 -3
- package/dist/esm/index.js +410 -34
- package/package.json +19 -14
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,6 +1,91 @@
|
|
|
1
|
-
import { FlowStore } from "@statelyai/flow";
|
|
1
|
+
import { FlowColorMode, FlowEngine, FlowInputEffects, FlowInputEvent, FlowStore, FlowStoreContext, Rect } from "@statelyai/flow";
|
|
2
2
|
|
|
3
|
+
//#region src/culling.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Node/edge ids worth mounting, or `null` meaning "render everything" — either
|
|
7
|
+
* because culling is disabled, the viewport hasn't been measured yet, or the
|
|
8
|
+
* whole graph already fits the viewport (so culling would mount everything
|
|
9
|
+
* anyway). Adapters mount exactly these ids; `null` means mount all.
|
|
10
|
+
*/
|
|
11
|
+
type RenderableIds = {
|
|
12
|
+
nodeIds: Set<string>;
|
|
13
|
+
edgeIds: Set<string>;
|
|
14
|
+
} | null;
|
|
15
|
+
/**
|
|
16
|
+
* Compare two `computeRenderableIds` results by contents. Adapters use this as
|
|
17
|
+
* the equality function for their reactive subscription so a layer only
|
|
18
|
+
* re-renders when the visible set actually changes — not on every store
|
|
19
|
+
* transition (e.g. every drag frame).
|
|
20
|
+
*/
|
|
21
|
+
declare function renderableIdsEqual(a: RenderableIds, b: RenderableIds): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Which entities should be mounted for the current viewport, or `null` to mount
|
|
24
|
+
* everything.
|
|
25
|
+
*
|
|
26
|
+
* Returns `null` when:
|
|
27
|
+
* - `enabled` is false (culling off),
|
|
28
|
+
* - the viewport is unmeasured (0×0 — culling against it would blank the first
|
|
29
|
+
* paint),
|
|
30
|
+
* - or the whole graph fits the viewport (+overscan). In that case culling
|
|
31
|
+
* would mount every entity anyway, so we skip the per-frame O(n+e) cull and
|
|
32
|
+
* the N-id Set build entirely — running it for zero benefit is what makes a
|
|
33
|
+
* zoomed-out drag laggy. The containment check is one O(n) `getGraphBounds`,
|
|
34
|
+
* far cheaper than the cull it avoids.
|
|
35
|
+
*
|
|
36
|
+
* Otherwise returns the visible node/edge id sets (viewport cull + overscan).
|
|
37
|
+
*
|
|
38
|
+
* `context` must be the engine's current snapshot (the engine reads its own
|
|
39
|
+
* `getGraphBounds`/`getRenderableEntities` from the live store) — which holds
|
|
40
|
+
* when called inside a store-subscription selector.
|
|
41
|
+
*/
|
|
42
|
+
declare function computeRenderableIds(engine: FlowEngine, context: FlowStoreContext, enabled: boolean | undefined): RenderableIds;
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/render-styles.d.ts
|
|
45
|
+
type NodePositionStyle = {
|
|
46
|
+
position: "absolute";
|
|
47
|
+
top: 0;
|
|
48
|
+
left: 0;
|
|
49
|
+
transform: string;
|
|
50
|
+
transformOrigin: "0 0";
|
|
51
|
+
willChange: "transform" | undefined;
|
|
52
|
+
width: number | "max-content";
|
|
53
|
+
height: number | undefined;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Position + size for a node's *wrapper* element. Always uses `transform`
|
|
57
|
+
* (compositor-friendly), never `left/top`, and GPU-promotes the wrapper while
|
|
58
|
+
* it is being dragged so moving it doesn't repaint its neighbours.
|
|
59
|
+
*
|
|
60
|
+
* `autoSize` nodes are sized by their content (the DOM owns the size and the
|
|
61
|
+
* measurement observer feeds it back); all others use the graph-owned size.
|
|
62
|
+
*
|
|
63
|
+
* This styles the wrapper box only — the node's content (including any custom
|
|
64
|
+
* component) renders inside and is untouched.
|
|
65
|
+
*/
|
|
66
|
+
declare function nodePositionStyle(bounds: Rect, options?: {
|
|
67
|
+
autoSize?: boolean;
|
|
68
|
+
dragging?: boolean;
|
|
69
|
+
}): NodePositionStyle;
|
|
70
|
+
type ViewportStyle = {
|
|
71
|
+
position: "absolute";
|
|
72
|
+
inset: 0;
|
|
73
|
+
transformOrigin: "0 0";
|
|
74
|
+
pointerEvents: "none";
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Style for the single transformed viewport element that wraps all canvas
|
|
78
|
+
* layers (nodes, edges, labels as static children). Its `transform` is set
|
|
79
|
+
* imperatively by `attachViewportTransform` and is deliberately NOT in this
|
|
80
|
+
* recipe — so a reactive framework binding can't re-render the node tree on
|
|
81
|
+
* every pan frame by owning the transform.
|
|
82
|
+
*/
|
|
83
|
+
declare function viewportStyle(): ViewportStyle;
|
|
84
|
+
//#endregion
|
|
3
85
|
//#region src/index.d.ts
|
|
86
|
+
type ResolvedFlowColorMode = "light" | "dark";
|
|
87
|
+
declare function resolveFlowColorMode(colorMode: FlowColorMode, targetWindow?: Pick<Window, "matchMedia"> | undefined): ResolvedFlowColorMode;
|
|
88
|
+
declare function applyFlowColorMode(element: HTMLElement, colorMode: FlowColorMode, targetWindow?: Pick<Window, "matchMedia"> | undefined): ResolvedFlowColorMode;
|
|
4
89
|
type MeasurementTarget = {
|
|
5
90
|
nodeId: string;
|
|
6
91
|
} | {
|
|
@@ -11,10 +96,42 @@ type MeasurementTarget = {
|
|
|
11
96
|
};
|
|
12
97
|
type MeasurementObserver = {
|
|
13
98
|
observe(element: Element, target: MeasurementTarget): void;
|
|
99
|
+
/** Re-measure now (position-only changes don't fire ResizeObserver). */
|
|
100
|
+
measure(element: Element): void;
|
|
14
101
|
unobserve(element: Element): void;
|
|
15
102
|
disconnect(): void;
|
|
16
103
|
};
|
|
17
|
-
|
|
104
|
+
type MeasurementObserverOptions = {
|
|
105
|
+
container?: Element | (() => Element | null | undefined) | null;
|
|
106
|
+
};
|
|
107
|
+
declare function createMeasurementObserver(store: FlowStore, options?: MeasurementObserverOptions): MeasurementObserver;
|
|
108
|
+
/** The container element registered for this store via `attachContainer`. */
|
|
109
|
+
declare function getAttachedContainer(store: FlowStore): Element | undefined;
|
|
18
110
|
declare function attachContainer(store: FlowStore, element: Element): () => void;
|
|
111
|
+
declare function attachViewportTransform(store: FlowStore, element: HTMLElement): () => void;
|
|
112
|
+
type AttachInputOptions = {
|
|
113
|
+
container: HTMLElement;
|
|
114
|
+
store: FlowStore;
|
|
115
|
+
runtime: {
|
|
116
|
+
send(event: FlowInputEvent): FlowInputEffects;
|
|
117
|
+
cancelSession(): void;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Auto-pan policy, evaluated once per animation frame while a gesture
|
|
121
|
+
* session holds the pointer near the container edge. Pass
|
|
122
|
+
* `getAutoPanDelta` from @statelyai/flow (injected as a function so this
|
|
123
|
+
* package keeps no runtime dependency on the optional peer). When it
|
|
124
|
+
* returns a delta, the binder pans the viewport and re-sends the
|
|
125
|
+
* stationary pointer so the dragged entity follows the canvas.
|
|
126
|
+
*/
|
|
127
|
+
autoPan?: (context: ReturnType<FlowStore["getSnapshot"]>["context"], screenPoint: {
|
|
128
|
+
x: number;
|
|
129
|
+
y: number;
|
|
130
|
+
}) => {
|
|
131
|
+
x: number;
|
|
132
|
+
y: number;
|
|
133
|
+
} | null;
|
|
134
|
+
};
|
|
135
|
+
declare function attachInput(options: AttachInputOptions): () => void;
|
|
19
136
|
//#endregion
|
|
20
|
-
export { MeasurementObserver, MeasurementTarget, attachContainer, createMeasurementObserver };
|
|
137
|
+
export { AttachInputOptions, MeasurementObserver, MeasurementObserverOptions, MeasurementTarget, type NodePositionStyle, type RenderableIds, ResolvedFlowColorMode, type ViewportStyle, applyFlowColorMode, attachContainer, attachInput, attachViewportTransform, computeRenderableIds, createMeasurementObserver, getAttachedContainer, nodePositionStyle, renderableIdsEqual, resolveFlowColorMode, viewportStyle };
|
package/dist/esm/index.js
CHANGED
|
@@ -1,54 +1,178 @@
|
|
|
1
|
+
//#region src/culling.ts
|
|
2
|
+
/** Overscan (canvas px) added around the viewport so entities just off-screen
|
|
3
|
+
* stay mounted and don't pop in/out at the edges. */
|
|
4
|
+
const CULL_OVERSCAN = 100;
|
|
5
|
+
function idSetsEqual(a, b) {
|
|
6
|
+
if (a.size !== b.size) return false;
|
|
7
|
+
for (const value of a) if (!b.has(value)) return false;
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Compare two `computeRenderableIds` results by contents. Adapters use this as
|
|
12
|
+
* the equality function for their reactive subscription so a layer only
|
|
13
|
+
* re-renders when the visible set actually changes — not on every store
|
|
14
|
+
* transition (e.g. every drag frame).
|
|
15
|
+
*/
|
|
16
|
+
function renderableIdsEqual(a, b) {
|
|
17
|
+
return a === b || a !== null && b !== null && idSetsEqual(a.nodeIds, b.nodeIds) && idSetsEqual(a.edgeIds, b.edgeIds);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Which entities should be mounted for the current viewport, or `null` to mount
|
|
21
|
+
* everything.
|
|
22
|
+
*
|
|
23
|
+
* Returns `null` when:
|
|
24
|
+
* - `enabled` is false (culling off),
|
|
25
|
+
* - the viewport is unmeasured (0×0 — culling against it would blank the first
|
|
26
|
+
* paint),
|
|
27
|
+
* - or the whole graph fits the viewport (+overscan). In that case culling
|
|
28
|
+
* would mount every entity anyway, so we skip the per-frame O(n+e) cull and
|
|
29
|
+
* the N-id Set build entirely — running it for zero benefit is what makes a
|
|
30
|
+
* zoomed-out drag laggy. The containment check is one O(n) `getGraphBounds`,
|
|
31
|
+
* far cheaper than the cull it avoids.
|
|
32
|
+
*
|
|
33
|
+
* Otherwise returns the visible node/edge id sets (viewport cull + overscan).
|
|
34
|
+
*
|
|
35
|
+
* `context` must be the engine's current snapshot (the engine reads its own
|
|
36
|
+
* `getGraphBounds`/`getRenderableEntities` from the live store) — which holds
|
|
37
|
+
* when called inside a store-subscription selector.
|
|
38
|
+
*/
|
|
39
|
+
function computeRenderableIds(engine, context, enabled) {
|
|
40
|
+
if (!enabled) return null;
|
|
41
|
+
const viewportSize = context.viewportSize;
|
|
42
|
+
if (viewportSize.width === 0 || viewportSize.height === 0) return null;
|
|
43
|
+
const viewport = context.viewport;
|
|
44
|
+
const left = -viewport.x / viewport.zoom - CULL_OVERSCAN;
|
|
45
|
+
const top = -viewport.y / viewport.zoom - CULL_OVERSCAN;
|
|
46
|
+
const right = left + viewportSize.width / viewport.zoom + CULL_OVERSCAN * 2;
|
|
47
|
+
const bottom = top + viewportSize.height / viewport.zoom + CULL_OVERSCAN * 2;
|
|
48
|
+
const bounds = engine.getGraphBounds();
|
|
49
|
+
if (left <= bounds.x && top <= bounds.y && right >= bounds.x + bounds.width && bottom >= bounds.y + bounds.height) return null;
|
|
50
|
+
const renderable = engine.getRenderableEntities({
|
|
51
|
+
cull: "viewport",
|
|
52
|
+
overscan: CULL_OVERSCAN
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
nodeIds: new Set(renderable.nodes.map((node) => node.id)),
|
|
56
|
+
edgeIds: new Set(renderable.edges.map((edge) => edge.id))
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/render-styles.ts
|
|
61
|
+
/**
|
|
62
|
+
* Position + size for a node's *wrapper* element. Always uses `transform`
|
|
63
|
+
* (compositor-friendly), never `left/top`, and GPU-promotes the wrapper while
|
|
64
|
+
* it is being dragged so moving it doesn't repaint its neighbours.
|
|
65
|
+
*
|
|
66
|
+
* `autoSize` nodes are sized by their content (the DOM owns the size and the
|
|
67
|
+
* measurement observer feeds it back); all others use the graph-owned size.
|
|
68
|
+
*
|
|
69
|
+
* This styles the wrapper box only — the node's content (including any custom
|
|
70
|
+
* component) renders inside and is untouched.
|
|
71
|
+
*/
|
|
72
|
+
function nodePositionStyle(bounds, options = {}) {
|
|
73
|
+
return {
|
|
74
|
+
position: "absolute",
|
|
75
|
+
top: 0,
|
|
76
|
+
left: 0,
|
|
77
|
+
transform: `translate(${bounds.x}px, ${bounds.y}px)`,
|
|
78
|
+
transformOrigin: "0 0",
|
|
79
|
+
willChange: options.dragging ? "transform" : void 0,
|
|
80
|
+
width: options.autoSize ? "max-content" : bounds.width,
|
|
81
|
+
height: options.autoSize ? void 0 : bounds.height
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Style for the single transformed viewport element that wraps all canvas
|
|
86
|
+
* layers (nodes, edges, labels as static children). Its `transform` is set
|
|
87
|
+
* imperatively by `attachViewportTransform` and is deliberately NOT in this
|
|
88
|
+
* recipe — so a reactive framework binding can't re-render the node tree on
|
|
89
|
+
* every pan frame by owning the transform.
|
|
90
|
+
*/
|
|
91
|
+
function viewportStyle() {
|
|
92
|
+
return {
|
|
93
|
+
position: "absolute",
|
|
94
|
+
inset: 0,
|
|
95
|
+
transformOrigin: "0 0",
|
|
96
|
+
pointerEvents: "none"
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
1
100
|
//#region src/index.ts
|
|
2
|
-
function
|
|
101
|
+
function resolveFlowColorMode(colorMode, targetWindow = typeof window === "undefined" ? void 0 : window) {
|
|
102
|
+
if (colorMode !== "system") return colorMode;
|
|
103
|
+
return targetWindow?.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
104
|
+
}
|
|
105
|
+
function applyFlowColorMode(element, colorMode, targetWindow = element.ownerDocument.defaultView ?? void 0) {
|
|
106
|
+
const resolvedColorMode = resolveFlowColorMode(colorMode, targetWindow);
|
|
107
|
+
element.dataset.flowColorMode = resolvedColorMode;
|
|
108
|
+
element.dataset.flowColorModePreference = colorMode;
|
|
109
|
+
element.style.colorScheme = resolvedColorMode;
|
|
110
|
+
return resolvedColorMode;
|
|
111
|
+
}
|
|
112
|
+
const containers = /* @__PURE__ */ new WeakMap();
|
|
113
|
+
function createMeasurementObserver(store, options = {}) {
|
|
3
114
|
const elementMap = /* @__PURE__ */ new Map();
|
|
4
|
-
let pendingMeasurements =
|
|
5
|
-
let pendingPorts = [];
|
|
115
|
+
let pendingMeasurements = [];
|
|
6
116
|
let rafId = null;
|
|
7
117
|
function flush() {
|
|
8
118
|
rafId = null;
|
|
9
|
-
if (pendingMeasurements.
|
|
10
|
-
store.trigger.
|
|
11
|
-
pendingMeasurements =
|
|
12
|
-
}
|
|
13
|
-
if (pendingPorts.length > 0) {
|
|
14
|
-
store.trigger.reportPortMeasurements({ measurements: pendingPorts });
|
|
15
|
-
pendingPorts = [];
|
|
119
|
+
if (pendingMeasurements.length > 0) {
|
|
120
|
+
store.trigger.updateMeasurements({ measurements: pendingMeasurements });
|
|
121
|
+
pendingMeasurements = [];
|
|
16
122
|
}
|
|
17
123
|
}
|
|
18
124
|
function scheduleFlush() {
|
|
19
125
|
if (rafId === null) rafId = requestAnimationFrame(flush);
|
|
20
126
|
}
|
|
127
|
+
function pushMeasurement(element, target, layoutSize) {
|
|
128
|
+
const viewport = store.getSnapshot().context.viewport;
|
|
129
|
+
const zoom = viewport.zoom || 1;
|
|
130
|
+
const rect = element.getBoundingClientRect();
|
|
131
|
+
const containerRect = (getContainer(options.container) ?? containers.get(store))?.getBoundingClientRect();
|
|
132
|
+
const measurement = {
|
|
133
|
+
x: (rect.left - (containerRect?.left ?? 0) - viewport.x) / zoom,
|
|
134
|
+
y: (rect.top - (containerRect?.top ?? 0) - viewport.y) / zoom,
|
|
135
|
+
width: layoutSize ? layoutSize.width : rect.width / zoom,
|
|
136
|
+
height: layoutSize ? layoutSize.height : rect.height / zoom
|
|
137
|
+
};
|
|
138
|
+
const id = "portName" in target ? `${target.nodeId}:${target.portName}` : "nodeId" in target ? target.nodeId : target.edgeId;
|
|
139
|
+
const existing = store.getSnapshot().context.measurements.get(id);
|
|
140
|
+
if (existing && Math.abs(existing.x - measurement.x) < .5 && Math.abs(existing.y - measurement.y) < .5 && Math.abs(existing.width - measurement.width) < .5 && Math.abs(existing.height - measurement.height) < .5) return;
|
|
141
|
+
pendingMeasurements.push({
|
|
142
|
+
id,
|
|
143
|
+
rect: measurement
|
|
144
|
+
});
|
|
145
|
+
scheduleFlush();
|
|
146
|
+
}
|
|
21
147
|
const observer = new ResizeObserver((entries) => {
|
|
22
148
|
for (const entry of entries) {
|
|
23
149
|
const target = elementMap.get(entry.target);
|
|
24
150
|
if (!target) continue;
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
height: rect.height
|
|
35
|
-
});
|
|
36
|
-
} else if ("nodeId" in target) pendingMeasurements.set(target.nodeId, {
|
|
37
|
-
width: rect.width,
|
|
38
|
-
height: rect.height
|
|
39
|
-
});
|
|
40
|
-
else pendingMeasurements.set(target.edgeId, {
|
|
41
|
-
width: rect.width,
|
|
42
|
-
height: rect.height
|
|
43
|
-
});
|
|
151
|
+
const box = entry.borderBoxSize?.[0];
|
|
152
|
+
const layoutSize = box ? {
|
|
153
|
+
width: box.inlineSize,
|
|
154
|
+
height: box.blockSize
|
|
155
|
+
} : entry.contentRect ? {
|
|
156
|
+
width: entry.contentRect.width,
|
|
157
|
+
height: entry.contentRect.height
|
|
158
|
+
} : void 0;
|
|
159
|
+
pushMeasurement(entry.target, target, layoutSize);
|
|
44
160
|
}
|
|
45
|
-
scheduleFlush();
|
|
46
161
|
});
|
|
47
162
|
return {
|
|
48
163
|
observe(element, target) {
|
|
49
164
|
elementMap.set(element, target);
|
|
50
165
|
observer.observe(element);
|
|
51
166
|
},
|
|
167
|
+
/**
|
|
168
|
+
* Re-measure an observed element immediately. ResizeObserver only fires
|
|
169
|
+
* on SIZE changes — position-only changes (layout passes, graph swaps,
|
|
170
|
+
* a port moving within its node) need an explicit re-measure.
|
|
171
|
+
*/
|
|
172
|
+
measure(element) {
|
|
173
|
+
const target = elementMap.get(element);
|
|
174
|
+
if (target) pushMeasurement(element, target);
|
|
175
|
+
},
|
|
52
176
|
unobserve(element) {
|
|
53
177
|
elementMap.delete(element);
|
|
54
178
|
observer.unobserve(element);
|
|
@@ -60,12 +184,19 @@ function createMeasurementObserver(store) {
|
|
|
60
184
|
cancelAnimationFrame(rafId);
|
|
61
185
|
rafId = null;
|
|
62
186
|
}
|
|
63
|
-
pendingMeasurements =
|
|
64
|
-
pendingPorts = [];
|
|
187
|
+
pendingMeasurements = [];
|
|
65
188
|
}
|
|
66
189
|
};
|
|
67
190
|
}
|
|
191
|
+
function getContainer(container) {
|
|
192
|
+
return typeof container === "function" ? container() ?? null : container ?? null;
|
|
193
|
+
}
|
|
194
|
+
/** The container element registered for this store via `attachContainer`. */
|
|
195
|
+
function getAttachedContainer(store) {
|
|
196
|
+
return containers.get(store);
|
|
197
|
+
}
|
|
68
198
|
function attachContainer(store, element) {
|
|
199
|
+
containers.set(store, element);
|
|
69
200
|
const update = () => {
|
|
70
201
|
const rect = element.getBoundingClientRect();
|
|
71
202
|
if (rect.width > 0 && rect.height > 0) store.trigger.updateViewportSize({
|
|
@@ -76,7 +207,252 @@ function attachContainer(store, element) {
|
|
|
76
207
|
update();
|
|
77
208
|
const observer = new ResizeObserver(update);
|
|
78
209
|
observer.observe(element);
|
|
79
|
-
return () =>
|
|
210
|
+
return () => {
|
|
211
|
+
if (containers.get(store) === element) containers.delete(store);
|
|
212
|
+
observer.disconnect();
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/** Idle delay (ms) before dropping the GPU-layer hint after interaction stops. */
|
|
216
|
+
const VIEWPORT_PROMOTE_IDLE_MS = 200;
|
|
217
|
+
function attachViewportTransform(store, element) {
|
|
218
|
+
let prevViewport = store.getSnapshot().context.viewport;
|
|
219
|
+
let prevState = store.getSnapshot().context.canvasState;
|
|
220
|
+
let idle;
|
|
221
|
+
const writeTransform = (viewport) => {
|
|
222
|
+
element.style.transform = `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`;
|
|
223
|
+
};
|
|
224
|
+
const promote = () => {
|
|
225
|
+
element.style.willChange = "transform";
|
|
226
|
+
if (idle !== void 0) clearTimeout(idle);
|
|
227
|
+
};
|
|
228
|
+
const scheduleClear = () => {
|
|
229
|
+
if (idle !== void 0) clearTimeout(idle);
|
|
230
|
+
idle = setTimeout(() => {
|
|
231
|
+
element.style.willChange = "auto";
|
|
232
|
+
}, VIEWPORT_PROMOTE_IDLE_MS);
|
|
233
|
+
};
|
|
234
|
+
writeTransform(prevViewport);
|
|
235
|
+
if (prevState !== "idle") promote();
|
|
236
|
+
const subscription = store.subscribe((snapshot) => {
|
|
237
|
+
const context = snapshot.context;
|
|
238
|
+
if (context.viewport !== prevViewport) {
|
|
239
|
+
prevViewport = context.viewport;
|
|
240
|
+
writeTransform(context.viewport);
|
|
241
|
+
promote();
|
|
242
|
+
if (context.canvasState === "idle") scheduleClear();
|
|
243
|
+
}
|
|
244
|
+
if (context.canvasState !== prevState) {
|
|
245
|
+
prevState = context.canvasState;
|
|
246
|
+
if (prevState !== "idle") promote();
|
|
247
|
+
else scheduleClear();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
return () => {
|
|
251
|
+
if (idle !== void 0) clearTimeout(idle);
|
|
252
|
+
subscription.unsubscribe();
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function attachInput(options) {
|
|
256
|
+
const { container, store, runtime } = options;
|
|
257
|
+
const doc = container.ownerDocument;
|
|
258
|
+
const win = doc.defaultView;
|
|
259
|
+
let capturedPointerId = null;
|
|
260
|
+
let lastClient = null;
|
|
261
|
+
let autoPanFrame = null;
|
|
262
|
+
function stopAutoPan() {
|
|
263
|
+
if (autoPanFrame !== null) {
|
|
264
|
+
win?.cancelAnimationFrame(autoPanFrame);
|
|
265
|
+
autoPanFrame = null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function autoPanTick() {
|
|
269
|
+
autoPanFrame = null;
|
|
270
|
+
if (!options.autoPan || capturedPointerId === null || !lastClient) return;
|
|
271
|
+
const rect = container.getBoundingClientRect();
|
|
272
|
+
const screenPoint = {
|
|
273
|
+
x: lastClient.x - rect.left,
|
|
274
|
+
y: lastClient.y - rect.top
|
|
275
|
+
};
|
|
276
|
+
const delta = options.autoPan(store.getSnapshot().context, screenPoint);
|
|
277
|
+
if (delta) {
|
|
278
|
+
store.trigger.panBy({
|
|
279
|
+
x: -delta.x,
|
|
280
|
+
y: -delta.y
|
|
281
|
+
});
|
|
282
|
+
sendPointer("pointer.move", lastClient.x, lastClient.y, {
|
|
283
|
+
pointerId: lastClient.pointerId,
|
|
284
|
+
button: -1,
|
|
285
|
+
modifiers: {
|
|
286
|
+
shift: false,
|
|
287
|
+
meta: false,
|
|
288
|
+
ctrl: false,
|
|
289
|
+
alt: false
|
|
290
|
+
},
|
|
291
|
+
target: null
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
scheduleAutoPan();
|
|
295
|
+
}
|
|
296
|
+
function scheduleAutoPan() {
|
|
297
|
+
if (options.autoPan && autoPanFrame === null && win) autoPanFrame = win.requestAnimationFrame(autoPanTick);
|
|
298
|
+
}
|
|
299
|
+
function modifiers(event) {
|
|
300
|
+
return {
|
|
301
|
+
shift: event.shiftKey,
|
|
302
|
+
meta: event.metaKey,
|
|
303
|
+
ctrl: event.ctrlKey,
|
|
304
|
+
alt: event.altKey
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function toPoints(event) {
|
|
308
|
+
const rect = container.getBoundingClientRect();
|
|
309
|
+
const screenPoint = {
|
|
310
|
+
x: event.clientX - rect.left,
|
|
311
|
+
y: event.clientY - rect.top
|
|
312
|
+
};
|
|
313
|
+
const { viewport } = store.getSnapshot().context;
|
|
314
|
+
const zoom = viewport.zoom || 1;
|
|
315
|
+
return {
|
|
316
|
+
screenPoint,
|
|
317
|
+
point: {
|
|
318
|
+
x: (screenPoint.x - viewport.x) / zoom,
|
|
319
|
+
y: (screenPoint.y - viewport.y) / zoom
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function resolveTarget(event) {
|
|
324
|
+
const element = capturedPointerId !== null ? doc.elementFromPoint(event.clientX, event.clientY) : event.target instanceof Element ? event.target : null;
|
|
325
|
+
if (!element) return { kind: "none" };
|
|
326
|
+
const portElement = element.closest("[data-flow-port-name]");
|
|
327
|
+
if (portElement?.dataset.flowPortName) return {
|
|
328
|
+
kind: "port",
|
|
329
|
+
nodeId: portElement.dataset.flowNodeId ?? "",
|
|
330
|
+
portName: portElement.dataset.flowPortName
|
|
331
|
+
};
|
|
332
|
+
if (element.closest("[data-flow-interactive]")) return { kind: "none" };
|
|
333
|
+
const entityElement = element.closest("[data-entity-id]");
|
|
334
|
+
if (entityElement?.dataset.entityId) {
|
|
335
|
+
const isLabel = element.closest("[data-flow-edge-label]") !== null;
|
|
336
|
+
const entityHasDragHandle = entityElement.querySelector("[data-flow-drag-handle]") !== null;
|
|
337
|
+
const pressedDragHandle = entityHasDragHandle && element.closest("[data-flow-drag-handle]") !== null;
|
|
338
|
+
return {
|
|
339
|
+
kind: "entity",
|
|
340
|
+
id: entityElement.dataset.entityId,
|
|
341
|
+
...isLabel ? { part: "label" } : {},
|
|
342
|
+
...entityHasDragHandle ? {
|
|
343
|
+
entityHasDragHandle,
|
|
344
|
+
pressedDragHandle
|
|
345
|
+
} : {}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
if (element === container || element.closest("[data-flow-canvas-layer]") !== null) return { kind: "pane" };
|
|
349
|
+
return { kind: "none" };
|
|
350
|
+
}
|
|
351
|
+
function applyEffects(event, effects) {
|
|
352
|
+
if (effects.preventDefault) event.preventDefault();
|
|
353
|
+
if (effects.stopPropagation) event.stopPropagation();
|
|
354
|
+
}
|
|
355
|
+
function sendPointer(kind, clientX, clientY, info) {
|
|
356
|
+
const { screenPoint, point } = toPoints({
|
|
357
|
+
clientX,
|
|
358
|
+
clientY
|
|
359
|
+
});
|
|
360
|
+
const effects = runtime.send({
|
|
361
|
+
kind,
|
|
362
|
+
pointerId: info.pointerId,
|
|
363
|
+
button: info.button,
|
|
364
|
+
point,
|
|
365
|
+
screenPoint,
|
|
366
|
+
target: resolveTarget({
|
|
367
|
+
clientX,
|
|
368
|
+
clientY,
|
|
369
|
+
target: info.target
|
|
370
|
+
}),
|
|
371
|
+
modifiers: info.modifiers
|
|
372
|
+
});
|
|
373
|
+
if (effects.sessionStarted && kind === "pointer.down") {
|
|
374
|
+
try {
|
|
375
|
+
container.setPointerCapture(info.pointerId);
|
|
376
|
+
} catch {}
|
|
377
|
+
capturedPointerId = info.pointerId;
|
|
378
|
+
scheduleAutoPan();
|
|
379
|
+
}
|
|
380
|
+
if (effects.sessionEnded && capturedPointerId !== null) {
|
|
381
|
+
try {
|
|
382
|
+
if (container.hasPointerCapture(capturedPointerId)) container.releasePointerCapture(capturedPointerId);
|
|
383
|
+
} catch {}
|
|
384
|
+
capturedPointerId = null;
|
|
385
|
+
lastClient = null;
|
|
386
|
+
stopAutoPan();
|
|
387
|
+
}
|
|
388
|
+
return effects;
|
|
389
|
+
}
|
|
390
|
+
function handlePointer(kind) {
|
|
391
|
+
return (event) => {
|
|
392
|
+
if (kind === "pointer.down" || capturedPointerId !== null && event.pointerId === capturedPointerId) lastClient = {
|
|
393
|
+
x: event.clientX,
|
|
394
|
+
y: event.clientY,
|
|
395
|
+
pointerId: event.pointerId
|
|
396
|
+
};
|
|
397
|
+
applyEffects(event, sendPointer(kind, event.clientX, event.clientY, {
|
|
398
|
+
pointerId: event.pointerId,
|
|
399
|
+
button: event.button,
|
|
400
|
+
modifiers: modifiers(event),
|
|
401
|
+
target: event.target
|
|
402
|
+
}));
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function handleWheel(event) {
|
|
406
|
+
const { screenPoint, point } = toPoints(event);
|
|
407
|
+
applyEffects(event, runtime.send({
|
|
408
|
+
kind: "wheel",
|
|
409
|
+
deltaX: event.deltaX,
|
|
410
|
+
deltaY: event.deltaY,
|
|
411
|
+
deltaMode: event.deltaMode,
|
|
412
|
+
point,
|
|
413
|
+
screenPoint,
|
|
414
|
+
target: resolveTarget(event),
|
|
415
|
+
modifiers: modifiers(event)
|
|
416
|
+
}));
|
|
417
|
+
}
|
|
418
|
+
function isEditableTarget(target) {
|
|
419
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
420
|
+
return target.isContentEditable || target.matches("input, textarea, select, [contenteditable='true']");
|
|
421
|
+
}
|
|
422
|
+
function handleKey(kind) {
|
|
423
|
+
return (event) => {
|
|
424
|
+
applyEffects(event, runtime.send({
|
|
425
|
+
kind,
|
|
426
|
+
key: event.key,
|
|
427
|
+
editableTarget: isEditableTarget(event.target),
|
|
428
|
+
modifiers: modifiers(event)
|
|
429
|
+
}));
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
const onPointerDown = handlePointer("pointer.down");
|
|
433
|
+
const onPointerMove = handlePointer("pointer.move");
|
|
434
|
+
const onPointerUp = handlePointer("pointer.up");
|
|
435
|
+
const onPointerCancel = handlePointer("pointer.cancel");
|
|
436
|
+
const onKeyDown = handleKey("key.down");
|
|
437
|
+
const onKeyUp = handleKey("key.up");
|
|
438
|
+
container.addEventListener("pointerdown", onPointerDown);
|
|
439
|
+
container.addEventListener("pointermove", onPointerMove);
|
|
440
|
+
container.addEventListener("pointerup", onPointerUp);
|
|
441
|
+
container.addEventListener("pointercancel", onPointerCancel);
|
|
442
|
+
container.addEventListener("wheel", handleWheel, { passive: false });
|
|
443
|
+
win?.addEventListener("keydown", onKeyDown);
|
|
444
|
+
win?.addEventListener("keyup", onKeyUp);
|
|
445
|
+
return () => {
|
|
446
|
+
stopAutoPan();
|
|
447
|
+
runtime.cancelSession();
|
|
448
|
+
container.removeEventListener("pointerdown", onPointerDown);
|
|
449
|
+
container.removeEventListener("pointermove", onPointerMove);
|
|
450
|
+
container.removeEventListener("pointerup", onPointerUp);
|
|
451
|
+
container.removeEventListener("pointercancel", onPointerCancel);
|
|
452
|
+
container.removeEventListener("wheel", handleWheel);
|
|
453
|
+
win?.removeEventListener("keydown", onKeyDown);
|
|
454
|
+
win?.removeEventListener("keyup", onKeyUp);
|
|
455
|
+
};
|
|
80
456
|
}
|
|
81
457
|
//#endregion
|
|
82
|
-
export { attachContainer, createMeasurementObserver };
|
|
458
|
+
export { applyFlowColorMode, attachContainer, attachInput, attachViewportTransform, computeRenderableIds, createMeasurementObserver, getAttachedContainer, nodePositionStyle, renderableIdsEqual, resolveFlowColorMode, viewportStyle };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statelyai/flow-dom",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "DOM-specific helpers for @statelyai/flow.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"flow",
|
|
@@ -17,27 +17,32 @@
|
|
|
17
17
|
"types": "dist/esm/index.d.ts",
|
|
18
18
|
"exports": {
|
|
19
19
|
".": {
|
|
20
|
+
"source": "./src/index.ts",
|
|
20
21
|
"types": "./dist/esm/index.d.ts",
|
|
21
22
|
"import": "./dist/esm/index.js",
|
|
22
23
|
"default": "./dist/esm/index.js"
|
|
23
|
-
},
|
|
24
|
-
"./src/index.ts": {
|
|
25
|
-
"types": "./src/index.ts",
|
|
26
|
-
"import": "./src/index.ts",
|
|
27
|
-
"default": "./src/index.ts"
|
|
28
24
|
}
|
|
29
25
|
},
|
|
30
26
|
"license": "MIT",
|
|
31
|
-
"scripts": {
|
|
32
|
-
"dev": "tsdown --watch",
|
|
33
|
-
"build": "tsdown",
|
|
34
|
-
"typecheck": "tsc --noEmit"
|
|
35
|
-
},
|
|
36
27
|
"peerDependencies": {
|
|
37
|
-
"@statelyai/flow": ">=0.
|
|
28
|
+
"@statelyai/flow": ">=0.4.2"
|
|
29
|
+
},
|
|
30
|
+
"peerDependenciesMeta": {
|
|
31
|
+
"@statelyai/flow": {
|
|
32
|
+
"optional": true
|
|
33
|
+
}
|
|
38
34
|
},
|
|
39
35
|
"devDependencies": {
|
|
36
|
+
"jsdom": "^25.0.0",
|
|
40
37
|
"tsdown": "^0.12.0",
|
|
41
|
-
"typescript": "^5.8.0"
|
|
38
|
+
"typescript": "^5.8.0",
|
|
39
|
+
"vitest": "^4.1.0",
|
|
40
|
+
"@statelyai/flow": "0.4.2"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"dev": "tsdown --watch",
|
|
44
|
+
"build": "tsdown",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"test": "vitest run"
|
|
42
47
|
}
|
|
43
|
-
}
|
|
48
|
+
}
|