@statelyai/flow-dom 3.0.1 → 3.2.0
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 +104 -6
- package/dist/esm/index.js +571 -26
- package/package.json +11 -13
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,7 +1,64 @@
|
|
|
1
|
-
import { FlowColorMode, FlowEngine, FlowInputEffects, FlowInputEvent, FlowStore, FlowStoreContext, Rect } from "@statelyai/flow";
|
|
2
|
-
|
|
1
|
+
import { EdgePathData, FlowColorMode, FlowController, FlowEngine, FlowEntityChangeNotifierOptions, FlowInputEffects, FlowInputEvent, FlowResult, FlowStore, FlowStoreContext, Point, Rect } from "@statelyai/flow";
|
|
2
|
+
export * from "@statelyai/flow";
|
|
3
|
+
//#region src/clipboard.d.ts
|
|
4
|
+
declare const FLOW_CONTENT_MIME = "application/vnd.stately.flow+json";
|
|
5
|
+
type ClipboardTransport = {
|
|
6
|
+
writeText?: (value: string) => Promise<void>;
|
|
7
|
+
readText?: () => Promise<string>;
|
|
8
|
+
write?: (items: unknown[]) => Promise<void>;
|
|
9
|
+
read?: () => Promise<Array<{
|
|
10
|
+
types: readonly string[];
|
|
11
|
+
getType(type: string): Promise<Blob>;
|
|
12
|
+
}>>;
|
|
13
|
+
};
|
|
14
|
+
type FlowClipboard = {
|
|
15
|
+
copy: (targets?: Iterable<string>) => Promise<boolean>;
|
|
16
|
+
cut: (targets?: Iterable<string>) => Promise<boolean>;
|
|
17
|
+
paste: (options?: {
|
|
18
|
+
point?: Point;
|
|
19
|
+
preservePosition?: boolean;
|
|
20
|
+
}) => Promise<FlowResult<unknown, Error>>;
|
|
21
|
+
};
|
|
22
|
+
declare function createFlowClipboard(flow: FlowController, options?: {
|
|
23
|
+
clipboard?: ClipboardTransport;
|
|
24
|
+
ClipboardItem?: new (items: Record<string, Blob>) => unknown;
|
|
25
|
+
}): FlowClipboard;
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/exportImage.d.ts
|
|
28
|
+
type FlowImageExportOptions = {
|
|
29
|
+
format?: 'svg' | 'png' | 'webp';
|
|
30
|
+
region?: 'graph' | 'viewport' | 'selection' | Rect;
|
|
31
|
+
padding?: number;
|
|
32
|
+
scale?: number;
|
|
33
|
+
pixelRatio?: number;
|
|
34
|
+
background?: string;
|
|
35
|
+
styles?: 'computed' | 'none';
|
|
36
|
+
include?: (element: Element) => boolean;
|
|
37
|
+
signal?: {
|
|
38
|
+
readonly aborted: boolean;
|
|
39
|
+
addEventListener(type: 'abort', listener: () => void, options?: {
|
|
40
|
+
once?: boolean;
|
|
41
|
+
}): void;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
type FlowImageExportResult = {
|
|
45
|
+
blob: Blob;
|
|
46
|
+
width: number;
|
|
47
|
+
height: number;
|
|
48
|
+
};
|
|
49
|
+
type FlowView = {
|
|
50
|
+
exportImage: (options?: FlowImageExportOptions) => Promise<FlowImageExportResult>;
|
|
51
|
+
refreshMeasurements: (targets?: Iterable<string | Element>) => void;
|
|
52
|
+
};
|
|
53
|
+
declare function createFlowView(container: HTMLElement, options?: {
|
|
54
|
+
measurements?: MeasurementObserver;
|
|
55
|
+
}): FlowView;
|
|
56
|
+
declare function exportFlowImage(container: HTMLElement, options?: FlowImageExportOptions): Promise<FlowImageExportResult>;
|
|
57
|
+
//#endregion
|
|
3
58
|
//#region src/culling.d.ts
|
|
4
|
-
|
|
59
|
+
type ComputeRenderableIdsOptions = {
|
|
60
|
+
overscan?: number;
|
|
61
|
+
};
|
|
5
62
|
/**
|
|
6
63
|
* Node/edge ids worth mounting, or `null` meaning "render everything" — either
|
|
7
64
|
* because culling is disabled, the viewport hasn't been measured yet, or the
|
|
@@ -39,7 +96,7 @@ declare function renderableIdsEqual(a: RenderableIds, b: RenderableIds): boolean
|
|
|
39
96
|
* `getGraphBounds`/`getRenderableEntities` from the live store) — which holds
|
|
40
97
|
* when called inside a store-subscription selector.
|
|
41
98
|
*/
|
|
42
|
-
declare function computeRenderableIds(engine: FlowEngine, context: FlowStoreContext, enabled: boolean | undefined): RenderableIds;
|
|
99
|
+
declare function computeRenderableIds(engine: FlowEngine, context: FlowStoreContext, enabled: boolean | undefined, options?: ComputeRenderableIdsOptions): RenderableIds;
|
|
43
100
|
//#endregion
|
|
44
101
|
//#region src/render-styles.d.ts
|
|
45
102
|
type NodePositionStyle = {
|
|
@@ -82,6 +139,34 @@ type ViewportStyle = {
|
|
|
82
139
|
*/
|
|
83
140
|
declare function viewportStyle(): ViewportStyle;
|
|
84
141
|
//#endregion
|
|
142
|
+
//#region src/entity-geometry.d.ts
|
|
143
|
+
type EntityRectTarget = {
|
|
144
|
+
id: string;
|
|
145
|
+
getRect: () => Rect | null;
|
|
146
|
+
autoSize?: boolean;
|
|
147
|
+
};
|
|
148
|
+
type EntityEdgeTarget = {
|
|
149
|
+
id: string;
|
|
150
|
+
getPathData: () => EdgePathData | null;
|
|
151
|
+
};
|
|
152
|
+
type EntityGeometryObserverOptions = FlowEntityChangeNotifierOptions & {
|
|
153
|
+
requestFrame?: (callback: FrameRequestCallback) => number;
|
|
154
|
+
cancelFrame?: (handle: number) => void;
|
|
155
|
+
};
|
|
156
|
+
type EntityGeometryObserver = {
|
|
157
|
+
observeRect(element: HTMLElement, target: EntityRectTarget): () => void;
|
|
158
|
+
observeEdge(element: SVGElement, target: EntityEdgeTarget): () => void;
|
|
159
|
+
/** Flush pending writes synchronously. Primarily useful for deterministic hosts/tests. */
|
|
160
|
+
flush(): void;
|
|
161
|
+
disconnect(): void;
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Batches transient entity geometry into imperative DOM writes. One core
|
|
165
|
+
* entity-change notifier keeps dispatch O(changed); framework adapters retain
|
|
166
|
+
* ownership of content, selection, and other durable rendering.
|
|
167
|
+
*/
|
|
168
|
+
declare function createEntityGeometryObserver(store: FlowStore, options?: EntityGeometryObserverOptions): EntityGeometryObserver;
|
|
169
|
+
//#endregion
|
|
85
170
|
//#region src/index.d.ts
|
|
86
171
|
type ResolvedFlowColorMode = "light" | "dark";
|
|
87
172
|
declare function resolveFlowColorMode(colorMode: FlowColorMode, targetWindow?: Pick<Window, "matchMedia"> | undefined): ResolvedFlowColorMode;
|
|
@@ -98,17 +183,25 @@ type MeasurementObserver = {
|
|
|
98
183
|
observe(element: Element, target: MeasurementTarget): void;
|
|
99
184
|
/** Re-measure now (position-only changes don't fire ResizeObserver). */
|
|
100
185
|
measure(element: Element): void;
|
|
186
|
+
refresh(targets?: Iterable<string | Element>): void;
|
|
101
187
|
unobserve(element: Element): void;
|
|
102
188
|
disconnect(): void;
|
|
103
189
|
};
|
|
104
190
|
type MeasurementObserverOptions = {
|
|
105
191
|
container?: Element | (() => Element | null | undefined) | null;
|
|
192
|
+
equalityThreshold?: number;
|
|
106
193
|
};
|
|
107
194
|
declare function createMeasurementObserver(store: FlowStore, options?: MeasurementObserverOptions): MeasurementObserver;
|
|
108
195
|
/** The container element registered for this store via `attachContainer`. */
|
|
109
196
|
declare function getAttachedContainer(store: FlowStore): Element | undefined;
|
|
197
|
+
declare function clientToCanvas(store: FlowStore, point: Point, container?: Element | null | undefined): Point;
|
|
198
|
+
declare function canvasToClient(store: FlowStore, point: Point, container?: Element | null | undefined): Point;
|
|
110
199
|
declare function attachContainer(store: FlowStore, element: Element): () => void;
|
|
111
|
-
|
|
200
|
+
type ViewportTransformOptions = {
|
|
201
|
+
/** Idle delay (ms) before dropping the GPU-layer hint after interaction stops. */
|
|
202
|
+
promoteIdleMs?: number;
|
|
203
|
+
};
|
|
204
|
+
declare function attachViewportTransform(store: FlowStore, element: HTMLElement, options?: ViewportTransformOptions): () => void;
|
|
112
205
|
type AttachInputOptions = {
|
|
113
206
|
container: HTMLElement;
|
|
114
207
|
store: FlowStore;
|
|
@@ -116,6 +209,11 @@ type AttachInputOptions = {
|
|
|
116
209
|
send(event: FlowInputEvent): FlowInputEffects;
|
|
117
210
|
cancelSession(): void;
|
|
118
211
|
};
|
|
212
|
+
/**
|
|
213
|
+
* Where keyboard events are listened for. Defaults to the flow container so
|
|
214
|
+
* multiple flow instances do not all react to global Delete/undo/arrow keys.
|
|
215
|
+
*/
|
|
216
|
+
keyboardTarget?: "window" | "container" | HTMLElement;
|
|
119
217
|
/**
|
|
120
218
|
* Auto-pan policy, evaluated once per animation frame while a gesture
|
|
121
219
|
* session holds the pointer near the container edge. Pass
|
|
@@ -134,4 +232,4 @@ type AttachInputOptions = {
|
|
|
134
232
|
};
|
|
135
233
|
declare function attachInput(options: AttachInputOptions): () => void;
|
|
136
234
|
//#endregion
|
|
137
|
-
export { AttachInputOptions, MeasurementObserver, MeasurementObserverOptions, MeasurementTarget, type NodePositionStyle, type RenderableIds, ResolvedFlowColorMode, type ViewportStyle, applyFlowColorMode, attachContainer, attachInput, attachViewportTransform, computeRenderableIds, createMeasurementObserver, getAttachedContainer, nodePositionStyle, renderableIdsEqual, resolveFlowColorMode, viewportStyle };
|
|
235
|
+
export { AttachInputOptions, type ClipboardTransport, type ComputeRenderableIdsOptions, type EntityEdgeTarget, type EntityGeometryObserver, type EntityGeometryObserverOptions, type EntityRectTarget, FLOW_CONTENT_MIME, type FlowClipboard, type FlowImageExportOptions, type FlowImageExportResult, type FlowView, MeasurementObserver, MeasurementObserverOptions, MeasurementTarget, type NodePositionStyle, type RenderableIds, ResolvedFlowColorMode, type ViewportStyle, ViewportTransformOptions, applyFlowColorMode, attachContainer, attachInput, attachViewportTransform, canvasToClient, clientToCanvas, computeRenderableIds, createEntityGeometryObserver, createFlowClipboard, createFlowView, createMeasurementObserver, exportFlowImage, getAttachedContainer, nodePositionStyle, renderableIdsEqual, resolveFlowColorMode, viewportStyle };
|
package/dist/esm/index.js
CHANGED
|
@@ -1,3 +1,237 @@
|
|
|
1
|
+
import { createEntityChangeNotifier, pathToSVG } from "@statelyai/flow";
|
|
2
|
+
export * from "@statelyai/flow";
|
|
3
|
+
//#region src/clipboard.ts
|
|
4
|
+
const FLOW_CONTENT_MIME = "application/vnd.stately.flow+json";
|
|
5
|
+
function createFlowClipboard(flow, options = {}) {
|
|
6
|
+
const clipboard = options.clipboard ?? globalThis.navigator?.clipboard;
|
|
7
|
+
let fallback = null;
|
|
8
|
+
async function write(content) {
|
|
9
|
+
fallback = content;
|
|
10
|
+
const json = JSON.stringify(content);
|
|
11
|
+
try {
|
|
12
|
+
if (clipboard?.write && options.ClipboardItem) {
|
|
13
|
+
const html = `<div data-stately-flow>${escapeHtml(json)}</div>`;
|
|
14
|
+
await clipboard.write([new options.ClipboardItem({
|
|
15
|
+
[FLOW_CONTENT_MIME]: new Blob([json], { type: FLOW_CONTENT_MIME }),
|
|
16
|
+
"text/plain": new Blob([json], { type: "text/plain" }),
|
|
17
|
+
"text/html": new Blob([html], { type: "text/html" })
|
|
18
|
+
})]);
|
|
19
|
+
} else if (clipboard?.writeText) await clipboard.writeText(json);
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function read() {
|
|
26
|
+
let readSucceeded = false;
|
|
27
|
+
try {
|
|
28
|
+
if (clipboard?.read) {
|
|
29
|
+
const items = await clipboard.read();
|
|
30
|
+
readSucceeded = true;
|
|
31
|
+
for (const item of items) {
|
|
32
|
+
const type = item.types.includes("application/vnd.stately.flow+json") ? FLOW_CONTENT_MIME : item.types.includes("text/plain") ? "text/plain" : null;
|
|
33
|
+
if (type) return parseJson(await (await item.getType(type)).text());
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (clipboard?.readText) {
|
|
37
|
+
const text = await clipboard.readText();
|
|
38
|
+
readSucceeded = true;
|
|
39
|
+
if (text) return parseJson(text);
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
if (readSucceeded) return null;
|
|
43
|
+
return fallback;
|
|
44
|
+
}
|
|
45
|
+
return readSucceeded ? null : fallback;
|
|
46
|
+
}
|
|
47
|
+
function canRun(command, targets) {
|
|
48
|
+
if (!targets) return flow.can(command);
|
|
49
|
+
return targets.length > 0 && targets.every((id) => flow.can(command, { id }));
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
async copy(targets) {
|
|
53
|
+
const ids = targets ? [...targets] : void 0;
|
|
54
|
+
if (!canRun("copy", ids)) return false;
|
|
55
|
+
const content = flow.content.extract(ids);
|
|
56
|
+
if (!content) return false;
|
|
57
|
+
await write(content);
|
|
58
|
+
return true;
|
|
59
|
+
},
|
|
60
|
+
async cut(targets) {
|
|
61
|
+
const ids = targets ? [...targets] : [...flow.getSnapshot().selection];
|
|
62
|
+
if (!canRun("cut", ids)) return false;
|
|
63
|
+
const content = flow.content.extract(ids);
|
|
64
|
+
if (!content) return false;
|
|
65
|
+
if (!await write(content)) return false;
|
|
66
|
+
flow.transaction((draft) => {
|
|
67
|
+
for (const id of ids) if (!draft.edges.delete(id)) draft.nodes.delete(id);
|
|
68
|
+
}, { origin: "clipboard.cut" });
|
|
69
|
+
return true;
|
|
70
|
+
},
|
|
71
|
+
async paste(insertOptions) {
|
|
72
|
+
const content = await read();
|
|
73
|
+
if (!content) return {
|
|
74
|
+
ok: false,
|
|
75
|
+
error: /* @__PURE__ */ new Error("Clipboard does not contain Flow content.")
|
|
76
|
+
};
|
|
77
|
+
return flow.content.insert(content, insertOptions);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function parseJson(value) {
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(value);
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function escapeHtml(value) {
|
|
89
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/exportImage.ts
|
|
93
|
+
function createFlowView(container, options = {}) {
|
|
94
|
+
return {
|
|
95
|
+
exportImage: (options) => exportFlowImage(container, options),
|
|
96
|
+
refreshMeasurements: (targets) => options.measurements?.refresh(targets)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async function exportFlowImage(container, options = {}) {
|
|
100
|
+
throwIfAborted(options.signal);
|
|
101
|
+
const win = container.ownerDocument.defaultView;
|
|
102
|
+
if (!win) throw new Error("Flow export requires a window.");
|
|
103
|
+
await nextFrame(win, options.signal);
|
|
104
|
+
const format = options.format ?? "svg";
|
|
105
|
+
const padding = Math.max(0, options.padding ?? 0);
|
|
106
|
+
const scale = Math.max(.01, options.scale ?? 1);
|
|
107
|
+
const pixelRatio = Math.max(.01, options.pixelRatio ?? win.devicePixelRatio ?? 1);
|
|
108
|
+
const region = resolveRegion(container, options.region ?? "graph");
|
|
109
|
+
const width = Math.max(1, Math.ceil((region.width + padding * 2) * scale));
|
|
110
|
+
const height = Math.max(1, Math.ceil((region.height + padding * 2) * scale));
|
|
111
|
+
const clone = container.cloneNode(true);
|
|
112
|
+
if ((options.styles ?? "computed") === "computed") inlineComputedStyles(win, container, clone);
|
|
113
|
+
if (options.include) {
|
|
114
|
+
const originals = [container, ...Array.from(container.querySelectorAll("*"))];
|
|
115
|
+
const clones = [clone, ...Array.from(clone.querySelectorAll("*"))];
|
|
116
|
+
for (let index = originals.length - 1; index >= 0; index--) if (!options.include(originals[index])) clones[index]?.remove();
|
|
117
|
+
}
|
|
118
|
+
const markup = new win.XMLSerializer().serializeToString(clone);
|
|
119
|
+
const background = options.background ? `<rect width="100%" height="100%" fill="${escapeXml(options.background)}"/>` : "";
|
|
120
|
+
const containerRect = container.getBoundingClientRect();
|
|
121
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="${region.x - padding} ${region.y - padding} ${region.width + padding * 2} ${region.height + padding * 2}">${background}<foreignObject x="0" y="0" width="${containerRect.width}" height="${containerRect.height}">${markup}</foreignObject></svg>`;
|
|
122
|
+
const svgBlob = new Blob([svg], { type: "image/svg+xml" });
|
|
123
|
+
if (format === "svg") return {
|
|
124
|
+
blob: svgBlob,
|
|
125
|
+
width,
|
|
126
|
+
height
|
|
127
|
+
};
|
|
128
|
+
throwIfAborted(options.signal);
|
|
129
|
+
const image = await loadImage(win, await blobDataUrl(win, svgBlob, options.signal), options.signal);
|
|
130
|
+
const canvas = container.ownerDocument.createElement("canvas");
|
|
131
|
+
canvas.width = Math.ceil(width * pixelRatio);
|
|
132
|
+
canvas.height = Math.ceil(height * pixelRatio);
|
|
133
|
+
const context = canvas.getContext("2d");
|
|
134
|
+
if (!context) throw new Error("Canvas 2D context is unavailable.");
|
|
135
|
+
context.scale(pixelRatio, pixelRatio);
|
|
136
|
+
context.drawImage(image, 0, 0, width, height);
|
|
137
|
+
return {
|
|
138
|
+
blob: await canvasBlob(canvas, `image/${format}`, options.signal),
|
|
139
|
+
width,
|
|
140
|
+
height
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function inlineComputedStyles(win, originalRoot, cloneRoot) {
|
|
144
|
+
const originals = [originalRoot, ...Array.from(originalRoot.querySelectorAll("*"))];
|
|
145
|
+
const clones = [cloneRoot, ...Array.from(cloneRoot.querySelectorAll("*"))];
|
|
146
|
+
for (let index = 0; index < originals.length; index++) {
|
|
147
|
+
const clone = clones[index];
|
|
148
|
+
if (!clone?.style) continue;
|
|
149
|
+
const computed = win.getComputedStyle(originals[index]);
|
|
150
|
+
for (let propertyIndex = 0; propertyIndex < computed.length; propertyIndex++) {
|
|
151
|
+
const property = computed.item(propertyIndex);
|
|
152
|
+
clone.style.setProperty(property, computed.getPropertyValue(property), computed.getPropertyPriority(property));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function blobDataUrl(win, blob, signal) {
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
const Reader = win.FileReader;
|
|
159
|
+
const reader = new Reader();
|
|
160
|
+
reader.onload = () => resolve(String(reader.result));
|
|
161
|
+
reader.onerror = () => reject(reader.error ?? /* @__PURE__ */ new Error("Unable to encode Flow SVG."));
|
|
162
|
+
signal?.addEventListener("abort", () => {
|
|
163
|
+
reader.abort();
|
|
164
|
+
reject(abortError());
|
|
165
|
+
}, { once: true });
|
|
166
|
+
reader.readAsDataURL(blob);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
function resolveRegion(container, region) {
|
|
170
|
+
if (typeof region === "object") return region;
|
|
171
|
+
const selector = region === "selection" ? "[data-flow-selected=\"true\"], [aria-selected=\"true\"]" : region === "graph" ? "[data-entity-id]" : null;
|
|
172
|
+
const elements = selector ? Array.from(container.querySelectorAll(selector)) : [container];
|
|
173
|
+
const containerRect = container.getBoundingClientRect();
|
|
174
|
+
if (elements.length === 0) return {
|
|
175
|
+
x: 0,
|
|
176
|
+
y: 0,
|
|
177
|
+
width: containerRect.width,
|
|
178
|
+
height: containerRect.height
|
|
179
|
+
};
|
|
180
|
+
const rects = elements.map((element) => element.getBoundingClientRect());
|
|
181
|
+
const left = Math.min(...rects.map((rect) => rect.left));
|
|
182
|
+
const top = Math.min(...rects.map((rect) => rect.top));
|
|
183
|
+
const right = Math.max(...rects.map((rect) => rect.right));
|
|
184
|
+
const bottom = Math.max(...rects.map((rect) => rect.bottom));
|
|
185
|
+
return {
|
|
186
|
+
x: left - containerRect.left,
|
|
187
|
+
y: top - containerRect.top,
|
|
188
|
+
width: right - left,
|
|
189
|
+
height: bottom - top
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function nextFrame(win, signal) {
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
const id = win.requestAnimationFrame(() => {
|
|
195
|
+
if (signal?.aborted) reject(abortError());
|
|
196
|
+
else resolve();
|
|
197
|
+
});
|
|
198
|
+
signal?.addEventListener("abort", () => {
|
|
199
|
+
win.cancelAnimationFrame(id);
|
|
200
|
+
reject(abortError());
|
|
201
|
+
}, { once: true });
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function loadImage(win, url, signal) {
|
|
205
|
+
return new Promise((resolve, reject) => {
|
|
206
|
+
const ImageCtor = win.Image;
|
|
207
|
+
const image = new ImageCtor();
|
|
208
|
+
image.onload = () => resolve(image);
|
|
209
|
+
image.onerror = () => reject(/* @__PURE__ */ new Error("Unable to render Flow SVG."));
|
|
210
|
+
signal?.addEventListener("abort", () => reject(abortError()), { once: true });
|
|
211
|
+
image.src = url;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
function canvasBlob(canvas, type, signal) {
|
|
215
|
+
return new Promise((resolve, reject) => {
|
|
216
|
+
canvas.toBlob((blob) => {
|
|
217
|
+
if (signal?.aborted) reject(abortError());
|
|
218
|
+
else if (blob) resolve(blob);
|
|
219
|
+
else reject(/* @__PURE__ */ new Error(`Unable to encode ${type}.`));
|
|
220
|
+
}, type);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function throwIfAborted(signal) {
|
|
224
|
+
if (signal?.aborted) throw abortError();
|
|
225
|
+
}
|
|
226
|
+
function abortError() {
|
|
227
|
+
const error = /* @__PURE__ */ new Error("The operation was aborted.");
|
|
228
|
+
error.name = "AbortError";
|
|
229
|
+
return error;
|
|
230
|
+
}
|
|
231
|
+
function escapeXml(value) {
|
|
232
|
+
return value.replaceAll("&", "&").replaceAll("\"", """).replaceAll("<", "<");
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
1
235
|
//#region src/culling.ts
|
|
2
236
|
/** Overscan (canvas px) added around the viewport so entities just off-screen
|
|
3
237
|
* stay mounted and don't pop in/out at the edges. */
|
|
@@ -36,20 +270,21 @@ function renderableIdsEqual(a, b) {
|
|
|
36
270
|
* `getGraphBounds`/`getRenderableEntities` from the live store) — which holds
|
|
37
271
|
* when called inside a store-subscription selector.
|
|
38
272
|
*/
|
|
39
|
-
function computeRenderableIds(engine, context, enabled) {
|
|
273
|
+
function computeRenderableIds(engine, context, enabled, options = {}) {
|
|
40
274
|
if (!enabled) return null;
|
|
275
|
+
const overscan = options.overscan ?? CULL_OVERSCAN;
|
|
41
276
|
const viewportSize = context.viewportSize;
|
|
42
277
|
if (viewportSize.width === 0 || viewportSize.height === 0) return null;
|
|
43
278
|
const viewport = context.viewport;
|
|
44
|
-
const left = -viewport.x / viewport.zoom -
|
|
45
|
-
const top = -viewport.y / viewport.zoom -
|
|
46
|
-
const right = left + viewportSize.width / viewport.zoom +
|
|
47
|
-
const bottom = top + viewportSize.height / viewport.zoom +
|
|
279
|
+
const left = -viewport.x / viewport.zoom - overscan;
|
|
280
|
+
const top = -viewport.y / viewport.zoom - overscan;
|
|
281
|
+
const right = left + viewportSize.width / viewport.zoom + overscan * 2;
|
|
282
|
+
const bottom = top + viewportSize.height / viewport.zoom + overscan * 2;
|
|
48
283
|
const bounds = engine.getGraphBounds();
|
|
49
284
|
if (left <= bounds.x && top <= bounds.y && right >= bounds.x + bounds.width && bottom >= bounds.y + bounds.height) return null;
|
|
50
285
|
const renderable = engine.getRenderableEntities({
|
|
51
286
|
cull: "viewport",
|
|
52
|
-
overscan
|
|
287
|
+
overscan
|
|
53
288
|
});
|
|
54
289
|
return {
|
|
55
290
|
nodeIds: new Set(renderable.nodes.map((node) => node.id)),
|
|
@@ -97,6 +332,107 @@ function viewportStyle() {
|
|
|
97
332
|
};
|
|
98
333
|
}
|
|
99
334
|
//#endregion
|
|
335
|
+
//#region src/entity-geometry.ts
|
|
336
|
+
/**
|
|
337
|
+
* Batches transient entity geometry into imperative DOM writes. One core
|
|
338
|
+
* entity-change notifier keeps dispatch O(changed); framework adapters retain
|
|
339
|
+
* ownership of content, selection, and other durable rendering.
|
|
340
|
+
*/
|
|
341
|
+
function createEntityGeometryObserver(store, options = {}) {
|
|
342
|
+
const requestFrame = options.requestFrame ?? ((callback) => requestAnimationFrame(callback));
|
|
343
|
+
const cancelFrame = options.cancelFrame ?? ((handle) => cancelAnimationFrame(handle));
|
|
344
|
+
const createNotifier = () => createEntityChangeNotifier(store, { getEdgeDependents: options.getEdgeDependents });
|
|
345
|
+
let notifier = createNotifier();
|
|
346
|
+
const getNotifier = () => notifier ??= createNotifier();
|
|
347
|
+
const observations = /* @__PURE__ */ new Set();
|
|
348
|
+
const pending = /* @__PURE__ */ new Set();
|
|
349
|
+
let frame = null;
|
|
350
|
+
const flush = () => {
|
|
351
|
+
if (frame !== null) {
|
|
352
|
+
cancelFrame(frame);
|
|
353
|
+
frame = null;
|
|
354
|
+
}
|
|
355
|
+
const current = [...pending];
|
|
356
|
+
pending.clear();
|
|
357
|
+
for (const observation of current) observation.write();
|
|
358
|
+
};
|
|
359
|
+
const schedule = (observation) => {
|
|
360
|
+
pending.add(observation);
|
|
361
|
+
if (frame === null) frame = requestFrame(() => {
|
|
362
|
+
frame = null;
|
|
363
|
+
const current = [...pending];
|
|
364
|
+
pending.clear();
|
|
365
|
+
for (const pendingObservation of current) pendingObservation.write();
|
|
366
|
+
});
|
|
367
|
+
};
|
|
368
|
+
const observe = (id, write) => {
|
|
369
|
+
const observation = {
|
|
370
|
+
write,
|
|
371
|
+
unsubscribe: () => {}
|
|
372
|
+
};
|
|
373
|
+
observation.unsubscribe = getNotifier().subscribe(id, () => schedule(observation));
|
|
374
|
+
observations.add(observation);
|
|
375
|
+
write();
|
|
376
|
+
return () => {
|
|
377
|
+
observation.unsubscribe();
|
|
378
|
+
observations.delete(observation);
|
|
379
|
+
pending.delete(observation);
|
|
380
|
+
};
|
|
381
|
+
};
|
|
382
|
+
return {
|
|
383
|
+
observeRect(element, target) {
|
|
384
|
+
return observe(target.id, () => {
|
|
385
|
+
const rect = target.getRect();
|
|
386
|
+
if (!rect) return;
|
|
387
|
+
const context = store.getSnapshot().context;
|
|
388
|
+
const dragging = context.canvasState === "dragging" && context.selection.has(target.id);
|
|
389
|
+
const resizing = context.resizingEntityId === target.id;
|
|
390
|
+
const layoutAnimating = context.layoutAnimation.status === "running" && context.layoutAnimation.offsets.has(target.id);
|
|
391
|
+
element.style.transform = `translate(${rect.x}px, ${rect.y}px)`;
|
|
392
|
+
if (!target.autoSize || resizing) {
|
|
393
|
+
element.style.width = `${rect.width}px`;
|
|
394
|
+
element.style.height = `${rect.height}px`;
|
|
395
|
+
}
|
|
396
|
+
if (dragging || resizing || layoutAnimating) element.style.willChange = resizing ? "transform, width, height" : "transform";
|
|
397
|
+
else element.style.removeProperty("will-change");
|
|
398
|
+
});
|
|
399
|
+
},
|
|
400
|
+
observeEdge(element, target) {
|
|
401
|
+
return observe(target.id, () => {
|
|
402
|
+
const pathData = target.getPathData();
|
|
403
|
+
if (!pathData) return;
|
|
404
|
+
element.querySelectorAll("[data-flow-edge-segment]").forEach((path) => {
|
|
405
|
+
const index = Number(path.dataset.flowEdgeSegment);
|
|
406
|
+
const segment = pathData.segments[index];
|
|
407
|
+
if (segment) path.setAttribute("d", pathToSVG(segment));
|
|
408
|
+
});
|
|
409
|
+
const first = pathData.segments[0]?.points[0];
|
|
410
|
+
const last = pathData.segments.at(-1)?.points.at(-1);
|
|
411
|
+
if (first) {
|
|
412
|
+
const source = element.querySelector("[data-flow-edge-endpoint=\"source\"]");
|
|
413
|
+
source?.setAttribute("cx", String(first.x));
|
|
414
|
+
source?.setAttribute("cy", String(first.y));
|
|
415
|
+
}
|
|
416
|
+
if (last) {
|
|
417
|
+
const targetElement = element.querySelector("[data-flow-edge-endpoint=\"target\"]");
|
|
418
|
+
targetElement?.setAttribute("cx", String(last.x));
|
|
419
|
+
targetElement?.setAttribute("cy", String(last.y));
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
},
|
|
423
|
+
flush,
|
|
424
|
+
disconnect() {
|
|
425
|
+
if (frame !== null) cancelFrame(frame);
|
|
426
|
+
frame = null;
|
|
427
|
+
pending.clear();
|
|
428
|
+
for (const observation of observations) observation.unsubscribe();
|
|
429
|
+
observations.clear();
|
|
430
|
+
notifier?.destroy();
|
|
431
|
+
notifier = null;
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
//#endregion
|
|
100
436
|
//#region src/index.ts
|
|
101
437
|
function resolveFlowColorMode(colorMode, targetWindow = typeof window === "undefined" ? void 0 : window) {
|
|
102
438
|
if (colorMode !== "system") return colorMode;
|
|
@@ -111,6 +447,7 @@ function applyFlowColorMode(element, colorMode, targetWindow = element.ownerDocu
|
|
|
111
447
|
}
|
|
112
448
|
const containers = /* @__PURE__ */ new WeakMap();
|
|
113
449
|
function createMeasurementObserver(store, options = {}) {
|
|
450
|
+
const equalityThreshold = options.equalityThreshold ?? .5;
|
|
114
451
|
const elementMap = /* @__PURE__ */ new Map();
|
|
115
452
|
let pendingMeasurements = [];
|
|
116
453
|
let rafId = null;
|
|
@@ -137,14 +474,14 @@ function createMeasurementObserver(store, options = {}) {
|
|
|
137
474
|
};
|
|
138
475
|
const id = "portName" in target ? `${target.nodeId}:${target.portName}` : "nodeId" in target ? target.nodeId : target.edgeId;
|
|
139
476
|
const existing = store.getSnapshot().context.measurements.get(id);
|
|
140
|
-
if (existing && Math.abs(existing.x - measurement.x) <
|
|
477
|
+
if (existing && Math.abs(existing.x - measurement.x) < equalityThreshold && Math.abs(existing.y - measurement.y) < equalityThreshold && Math.abs(existing.width - measurement.width) < equalityThreshold && Math.abs(existing.height - measurement.height) < equalityThreshold) return;
|
|
141
478
|
pendingMeasurements.push({
|
|
142
479
|
id,
|
|
143
480
|
rect: measurement
|
|
144
481
|
});
|
|
145
482
|
scheduleFlush();
|
|
146
483
|
}
|
|
147
|
-
const
|
|
484
|
+
const handleResize = (entries) => {
|
|
148
485
|
for (const entry of entries) {
|
|
149
486
|
const target = elementMap.get(entry.target);
|
|
150
487
|
if (!target) continue;
|
|
@@ -158,11 +495,17 @@ function createMeasurementObserver(store, options = {}) {
|
|
|
158
495
|
} : void 0;
|
|
159
496
|
pushMeasurement(entry.target, target, layoutSize);
|
|
160
497
|
}
|
|
161
|
-
}
|
|
498
|
+
};
|
|
499
|
+
let observer = null;
|
|
500
|
+
function getObserver(element) {
|
|
501
|
+
if (observer) return observer;
|
|
502
|
+
observer = new ((element.ownerDocument.defaultView?.ResizeObserver) ?? ResizeObserver)(handleResize);
|
|
503
|
+
return observer;
|
|
504
|
+
}
|
|
162
505
|
return {
|
|
163
506
|
observe(element, target) {
|
|
164
507
|
elementMap.set(element, target);
|
|
165
|
-
|
|
508
|
+
getObserver(element).observe(element);
|
|
166
509
|
},
|
|
167
510
|
/**
|
|
168
511
|
* Re-measure an observed element immediately. ResizeObserver only fires
|
|
@@ -173,13 +516,22 @@ function createMeasurementObserver(store, options = {}) {
|
|
|
173
516
|
const target = elementMap.get(element);
|
|
174
517
|
if (target) pushMeasurement(element, target);
|
|
175
518
|
},
|
|
519
|
+
refresh(targets) {
|
|
520
|
+
const requested = targets ? new Set(targets) : null;
|
|
521
|
+
for (const [element, target] of elementMap) {
|
|
522
|
+
const id = "portName" in target ? `${target.nodeId}:${target.portName}` : "nodeId" in target ? target.nodeId : target.edgeId;
|
|
523
|
+
if (requested && !requested.has(element) && !requested.has(id)) continue;
|
|
524
|
+
pushMeasurement(element, target);
|
|
525
|
+
}
|
|
526
|
+
},
|
|
176
527
|
unobserve(element) {
|
|
177
528
|
elementMap.delete(element);
|
|
178
|
-
observer
|
|
529
|
+
observer?.unobserve(element);
|
|
179
530
|
},
|
|
180
531
|
disconnect() {
|
|
181
532
|
elementMap.clear();
|
|
182
|
-
observer
|
|
533
|
+
observer?.disconnect();
|
|
534
|
+
observer = null;
|
|
183
535
|
if (rafId !== null) {
|
|
184
536
|
cancelAnimationFrame(rafId);
|
|
185
537
|
rafId = null;
|
|
@@ -195,6 +547,23 @@ function getContainer(container) {
|
|
|
195
547
|
function getAttachedContainer(store) {
|
|
196
548
|
return containers.get(store);
|
|
197
549
|
}
|
|
550
|
+
function clientToCanvas(store, point, container = containers.get(store)) {
|
|
551
|
+
const rect = container?.getBoundingClientRect();
|
|
552
|
+
const viewport = store.getSnapshot().context.viewport;
|
|
553
|
+
const zoom = viewport.zoom || 1;
|
|
554
|
+
return {
|
|
555
|
+
x: (point.x - (rect?.left ?? 0) - viewport.x) / zoom,
|
|
556
|
+
y: (point.y - (rect?.top ?? 0) - viewport.y) / zoom
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
function canvasToClient(store, point, container = containers.get(store)) {
|
|
560
|
+
const rect = container?.getBoundingClientRect();
|
|
561
|
+
const viewport = store.getSnapshot().context.viewport;
|
|
562
|
+
return {
|
|
563
|
+
x: point.x * viewport.zoom + viewport.x + (rect?.left ?? 0),
|
|
564
|
+
y: point.y * viewport.zoom + viewport.y + (rect?.top ?? 0)
|
|
565
|
+
};
|
|
566
|
+
}
|
|
198
567
|
function attachContainer(store, element) {
|
|
199
568
|
containers.set(store, element);
|
|
200
569
|
const update = () => {
|
|
@@ -205,21 +574,22 @@ function attachContainer(store, element) {
|
|
|
205
574
|
});
|
|
206
575
|
};
|
|
207
576
|
update();
|
|
208
|
-
const observer = new ResizeObserver(update);
|
|
577
|
+
const observer = new ((element.ownerDocument.defaultView?.ResizeObserver) ?? ResizeObserver)(update);
|
|
209
578
|
observer.observe(element);
|
|
210
579
|
return () => {
|
|
211
580
|
if (containers.get(store) === element) containers.delete(store);
|
|
212
581
|
observer.disconnect();
|
|
213
582
|
};
|
|
214
583
|
}
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
function attachViewportTransform(store, element) {
|
|
584
|
+
function attachViewportTransform(store, element, options = {}) {
|
|
585
|
+
const promoteIdleMs = options.promoteIdleMs ?? 200;
|
|
218
586
|
let prevViewport = store.getSnapshot().context.viewport;
|
|
219
587
|
let prevState = store.getSnapshot().context.canvasState;
|
|
220
588
|
let idle;
|
|
221
589
|
const writeTransform = (viewport) => {
|
|
222
590
|
element.style.transform = `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`;
|
|
591
|
+
element.style.setProperty("--flow-zoom", String(viewport.zoom));
|
|
592
|
+
element.style.setProperty("--flow-inverse-zoom", String(1 / viewport.zoom));
|
|
223
593
|
};
|
|
224
594
|
const promote = () => {
|
|
225
595
|
element.style.willChange = "transform";
|
|
@@ -229,7 +599,7 @@ function attachViewportTransform(store, element) {
|
|
|
229
599
|
if (idle !== void 0) clearTimeout(idle);
|
|
230
600
|
idle = setTimeout(() => {
|
|
231
601
|
element.style.willChange = "auto";
|
|
232
|
-
},
|
|
602
|
+
}, promoteIdleMs);
|
|
233
603
|
};
|
|
234
604
|
writeTransform(prevViewport);
|
|
235
605
|
if (prevState !== "idle") promote();
|
|
@@ -256,9 +626,20 @@ function attachInput(options) {
|
|
|
256
626
|
const { container, store, runtime } = options;
|
|
257
627
|
const doc = container.ownerDocument;
|
|
258
628
|
const win = doc.defaultView;
|
|
629
|
+
const ElementCtor = win?.Element;
|
|
630
|
+
const HTMLElementCtor = win?.HTMLElement;
|
|
631
|
+
const keyboardTarget = options.keyboardTarget ?? "container";
|
|
632
|
+
const keyboardElement = keyboardTarget === "window" ? win : keyboardTarget === "container" ? container : keyboardTarget;
|
|
259
633
|
let capturedPointerId = null;
|
|
260
634
|
let lastClient = null;
|
|
261
635
|
let autoPanFrame = null;
|
|
636
|
+
const activeTouchPointers = /* @__PURE__ */ new Map();
|
|
637
|
+
let pinchActive = false;
|
|
638
|
+
const DOUBLE_CLICK_MS = 500;
|
|
639
|
+
const DOUBLE_CLICK_DISTANCE = 4;
|
|
640
|
+
let lastClickDown = null;
|
|
641
|
+
let clickCount = 0;
|
|
642
|
+
let lastPinch = null;
|
|
262
643
|
function stopAutoPan() {
|
|
263
644
|
if (autoPanFrame !== null) {
|
|
264
645
|
win?.cancelAnimationFrame(autoPanFrame);
|
|
@@ -304,6 +685,18 @@ function attachInput(options) {
|
|
|
304
685
|
alt: event.altKey
|
|
305
686
|
};
|
|
306
687
|
}
|
|
688
|
+
function computeClickDetail(event) {
|
|
689
|
+
if (event.pointerType === "touch") return event.detail || 0;
|
|
690
|
+
const now = event.timeStamp || (win ? win.performance.now() : Date.now());
|
|
691
|
+
if (lastClickDown && now - lastClickDown.time <= DOUBLE_CLICK_MS && Math.abs(event.clientX - lastClickDown.x) <= DOUBLE_CLICK_DISTANCE && Math.abs(event.clientY - lastClickDown.y) <= DOUBLE_CLICK_DISTANCE) clickCount += 1;
|
|
692
|
+
else clickCount = 1;
|
|
693
|
+
lastClickDown = {
|
|
694
|
+
time: now,
|
|
695
|
+
x: event.clientX,
|
|
696
|
+
y: event.clientY
|
|
697
|
+
};
|
|
698
|
+
return clickCount;
|
|
699
|
+
}
|
|
307
700
|
function toPoints(event) {
|
|
308
701
|
const rect = container.getBoundingClientRect();
|
|
309
702
|
const screenPoint = {
|
|
@@ -321,24 +714,28 @@ function attachInput(options) {
|
|
|
321
714
|
};
|
|
322
715
|
}
|
|
323
716
|
function resolveTarget(event) {
|
|
324
|
-
const element = capturedPointerId !== null ? doc.elementFromPoint(event.clientX, event.clientY) : event.target instanceof
|
|
717
|
+
const element = capturedPointerId !== null ? doc.elementFromPoint(event.clientX, event.clientY) : ElementCtor && event.target instanceof ElementCtor ? event.target : null;
|
|
325
718
|
if (!element) return { kind: "none" };
|
|
719
|
+
if (!container.contains(element)) return { kind: "none" };
|
|
720
|
+
const owningRoot = element.closest("[data-flow-root]");
|
|
721
|
+
if (owningRoot && owningRoot !== container) return { kind: "none" };
|
|
326
722
|
const portElement = element.closest("[data-flow-port-name]");
|
|
327
723
|
if (portElement?.dataset.flowPortName) return {
|
|
328
724
|
kind: "port",
|
|
329
725
|
nodeId: portElement.dataset.flowNodeId ?? "",
|
|
330
726
|
portName: portElement.dataset.flowPortName
|
|
331
727
|
};
|
|
332
|
-
if (element.closest("[data-flow-interactive]")) return { kind: "none" };
|
|
728
|
+
if (element.closest("[data-flow-interactive], .nodrag, input, textarea, select, button, a[href], [contenteditable]:not([contenteditable=\"false\"])")) return { kind: "none" };
|
|
333
729
|
const entityElement = element.closest("[data-entity-id]");
|
|
334
730
|
if (entityElement?.dataset.entityId) {
|
|
335
731
|
const isLabel = element.closest("[data-flow-edge-label]") !== null;
|
|
732
|
+
const part = element.closest("[data-flow-part]")?.dataset.flowPart ?? (isLabel ? "label" : void 0);
|
|
336
733
|
const entityHasDragHandle = entityElement.querySelector("[data-flow-drag-handle]") !== null;
|
|
337
734
|
const pressedDragHandle = entityHasDragHandle && element.closest("[data-flow-drag-handle]") !== null;
|
|
338
735
|
return {
|
|
339
736
|
kind: "entity",
|
|
340
737
|
id: entityElement.dataset.entityId,
|
|
341
|
-
...
|
|
738
|
+
...part ? { part } : {},
|
|
342
739
|
...entityHasDragHandle ? {
|
|
343
740
|
entityHasDragHandle,
|
|
344
741
|
pressedDragHandle
|
|
@@ -360,7 +757,9 @@ function attachInput(options) {
|
|
|
360
757
|
const effects = runtime.send({
|
|
361
758
|
kind,
|
|
362
759
|
pointerId: info.pointerId,
|
|
760
|
+
pointerType: info.pointerType ?? "mouse",
|
|
363
761
|
button: info.button,
|
|
762
|
+
detail: info.detail,
|
|
364
763
|
point,
|
|
365
764
|
screenPoint,
|
|
366
765
|
target: resolveTarget({
|
|
@@ -387,16 +786,116 @@ function attachInput(options) {
|
|
|
387
786
|
}
|
|
388
787
|
return effects;
|
|
389
788
|
}
|
|
789
|
+
function touchPoints() {
|
|
790
|
+
return [...activeTouchPointers.values()];
|
|
791
|
+
}
|
|
792
|
+
function pinchGeometry() {
|
|
793
|
+
const touches = touchPoints();
|
|
794
|
+
if (touches.length < 2) return null;
|
|
795
|
+
const [a, b] = touches;
|
|
796
|
+
const screenCenter = {
|
|
797
|
+
x: (a.clientX + b.clientX) / 2 - container.getBoundingClientRect().left,
|
|
798
|
+
y: (a.clientY + b.clientY) / 2 - container.getBoundingClientRect().top
|
|
799
|
+
};
|
|
800
|
+
const { point: center } = toPoints({
|
|
801
|
+
clientX: (a.clientX + b.clientX) / 2,
|
|
802
|
+
clientY: (a.clientY + b.clientY) / 2
|
|
803
|
+
});
|
|
804
|
+
return {
|
|
805
|
+
screenCenter,
|
|
806
|
+
center,
|
|
807
|
+
distance: Math.hypot(b.clientX - a.clientX, b.clientY - a.clientY),
|
|
808
|
+
modifiers: a.modifiers
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
function sendPinch(kind) {
|
|
812
|
+
const fallback = (kind === "pinch.end" ? lastPinch : pinchGeometry()) ?? lastPinch;
|
|
813
|
+
if (!fallback) return makeNoEffects();
|
|
814
|
+
lastPinch = fallback;
|
|
815
|
+
return runtime.send({
|
|
816
|
+
kind,
|
|
817
|
+
center: fallback.center,
|
|
818
|
+
screenCenter: fallback.screenCenter,
|
|
819
|
+
distance: fallback.distance,
|
|
820
|
+
pointerCount: activeTouchPointers.size,
|
|
821
|
+
modifiers: fallback.modifiers
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
function makeNoEffects() {
|
|
825
|
+
return {
|
|
826
|
+
preventDefault: false,
|
|
827
|
+
stopPropagation: false,
|
|
828
|
+
sessionStarted: false,
|
|
829
|
+
sessionEnded: false,
|
|
830
|
+
handledBy: null
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
function releaseCapturedPointer() {
|
|
834
|
+
if (capturedPointerId === null) return;
|
|
835
|
+
try {
|
|
836
|
+
if (container.hasPointerCapture(capturedPointerId)) container.releasePointerCapture(capturedPointerId);
|
|
837
|
+
} catch {}
|
|
838
|
+
capturedPointerId = null;
|
|
839
|
+
lastClient = null;
|
|
840
|
+
stopAutoPan();
|
|
841
|
+
}
|
|
390
842
|
function handlePointer(kind) {
|
|
391
843
|
return (event) => {
|
|
844
|
+
if (kind === "pointer.down") focusContainerForPanePress(event);
|
|
845
|
+
const pointerType = event.pointerType === "touch" || event.pointerType === "pen" || event.pointerType === "mouse" ? event.pointerType : "mouse";
|
|
846
|
+
if (pointerType === "touch") {
|
|
847
|
+
if (kind === "pointer.down") {
|
|
848
|
+
activeTouchPointers.set(event.pointerId, {
|
|
849
|
+
clientX: event.clientX,
|
|
850
|
+
clientY: event.clientY,
|
|
851
|
+
modifiers: modifiers(event)
|
|
852
|
+
});
|
|
853
|
+
if (activeTouchPointers.size === 2) {
|
|
854
|
+
runtime.cancelSession();
|
|
855
|
+
releaseCapturedPointer();
|
|
856
|
+
pinchActive = true;
|
|
857
|
+
lastPinch = pinchGeometry();
|
|
858
|
+
applyEffects(event, sendPinch("pinch.start"));
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (activeTouchPointers.size > 2) {
|
|
862
|
+
applyEffects(event, sendPinch("pinch.move"));
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
} else if (activeTouchPointers.has(event.pointerId)) {
|
|
866
|
+
if (kind === "pointer.move") {
|
|
867
|
+
activeTouchPointers.set(event.pointerId, {
|
|
868
|
+
clientX: event.clientX,
|
|
869
|
+
clientY: event.clientY,
|
|
870
|
+
modifiers: modifiers(event)
|
|
871
|
+
});
|
|
872
|
+
if (pinchActive) {
|
|
873
|
+
applyEffects(event, sendPinch("pinch.move"));
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
} else if (kind === "pointer.up" || kind === "pointer.cancel") {
|
|
877
|
+
activeTouchPointers.delete(event.pointerId);
|
|
878
|
+
if (pinchActive) {
|
|
879
|
+
const effects = sendPinch("pinch.end");
|
|
880
|
+
pinchActive = false;
|
|
881
|
+
lastPinch = null;
|
|
882
|
+
applyEffects(event, effects);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
392
888
|
if (kind === "pointer.down" || capturedPointerId !== null && event.pointerId === capturedPointerId) lastClient = {
|
|
393
889
|
x: event.clientX,
|
|
394
890
|
y: event.clientY,
|
|
395
891
|
pointerId: event.pointerId
|
|
396
892
|
};
|
|
893
|
+
const detail = kind === "pointer.down" && event.button === 0 ? computeClickDetail(event) : event.detail;
|
|
397
894
|
applyEffects(event, sendPointer(kind, event.clientX, event.clientY, {
|
|
398
895
|
pointerId: event.pointerId,
|
|
896
|
+
pointerType,
|
|
399
897
|
button: event.button,
|
|
898
|
+
detail,
|
|
400
899
|
modifiers: modifiers(event),
|
|
401
900
|
target: event.target
|
|
402
901
|
}));
|
|
@@ -415,10 +914,33 @@ function attachInput(options) {
|
|
|
415
914
|
modifiers: modifiers(event)
|
|
416
915
|
}));
|
|
417
916
|
}
|
|
917
|
+
function handleContextMenu(event) {
|
|
918
|
+
applyEffects(event, sendPointer("pointer.down", event.clientX, event.clientY, {
|
|
919
|
+
pointerId: -1,
|
|
920
|
+
pointerType: "mouse",
|
|
921
|
+
button: 2,
|
|
922
|
+
detail: event.detail,
|
|
923
|
+
modifiers: modifiers(event),
|
|
924
|
+
target: event.target
|
|
925
|
+
}));
|
|
926
|
+
}
|
|
418
927
|
function isEditableTarget(target) {
|
|
419
|
-
if (!(target instanceof
|
|
928
|
+
if (!HTMLElementCtor || !(target instanceof HTMLElementCtor)) return false;
|
|
420
929
|
return target.isContentEditable || target.matches("input, textarea, select, [contenteditable='true']");
|
|
421
930
|
}
|
|
931
|
+
function isInteractiveTarget(target) {
|
|
932
|
+
return !!ElementCtor && target instanceof ElementCtor && target.closest("[data-flow-interactive]") !== null;
|
|
933
|
+
}
|
|
934
|
+
function focusContainerForPanePress(event) {
|
|
935
|
+
if (keyboardTarget !== "container") return;
|
|
936
|
+
if (isEditableTarget(event.target) || isInteractiveTarget(event.target)) return;
|
|
937
|
+
if (resolveTarget({
|
|
938
|
+
clientX: event.clientX,
|
|
939
|
+
clientY: event.clientY,
|
|
940
|
+
target: event.target
|
|
941
|
+
}).kind !== "pane") return;
|
|
942
|
+
container.focus({ preventScroll: true });
|
|
943
|
+
}
|
|
422
944
|
function handleKey(kind) {
|
|
423
945
|
return (event) => {
|
|
424
946
|
applyEffects(event, runtime.send({
|
|
@@ -433,26 +955,49 @@ function attachInput(options) {
|
|
|
433
955
|
const onPointerMove = handlePointer("pointer.move");
|
|
434
956
|
const onPointerUp = handlePointer("pointer.up");
|
|
435
957
|
const onPointerCancel = handlePointer("pointer.cancel");
|
|
958
|
+
const onLostPointerCapture = () => {
|
|
959
|
+
stopAutoPan();
|
|
960
|
+
runtime.cancelSession();
|
|
961
|
+
capturedPointerId = null;
|
|
962
|
+
lastClient = null;
|
|
963
|
+
};
|
|
964
|
+
const onWindowBlur = () => {
|
|
965
|
+
stopAutoPan();
|
|
966
|
+
runtime.cancelSession();
|
|
967
|
+
activeTouchPointers.clear();
|
|
968
|
+
capturedPointerId = null;
|
|
969
|
+
lastClient = null;
|
|
970
|
+
};
|
|
436
971
|
const onKeyDown = handleKey("key.down");
|
|
437
972
|
const onKeyUp = handleKey("key.up");
|
|
973
|
+
if (keyboardTarget === "container" && !container.hasAttribute("tabindex")) container.tabIndex = 0;
|
|
438
974
|
container.addEventListener("pointerdown", onPointerDown);
|
|
439
975
|
container.addEventListener("pointermove", onPointerMove);
|
|
440
976
|
container.addEventListener("pointerup", onPointerUp);
|
|
441
977
|
container.addEventListener("pointercancel", onPointerCancel);
|
|
978
|
+
container.addEventListener("lostpointercapture", onLostPointerCapture);
|
|
979
|
+
container.addEventListener("contextmenu", handleContextMenu);
|
|
442
980
|
container.addEventListener("wheel", handleWheel, { passive: false });
|
|
443
|
-
|
|
444
|
-
|
|
981
|
+
keyboardElement?.addEventListener("keydown", onKeyDown);
|
|
982
|
+
keyboardElement?.addEventListener("keyup", onKeyUp);
|
|
983
|
+
win?.addEventListener("blur", onWindowBlur);
|
|
445
984
|
return () => {
|
|
446
985
|
stopAutoPan();
|
|
447
986
|
runtime.cancelSession();
|
|
987
|
+
activeTouchPointers.clear();
|
|
988
|
+
pinchActive = false;
|
|
989
|
+
lastPinch = null;
|
|
448
990
|
container.removeEventListener("pointerdown", onPointerDown);
|
|
449
991
|
container.removeEventListener("pointermove", onPointerMove);
|
|
450
992
|
container.removeEventListener("pointerup", onPointerUp);
|
|
451
993
|
container.removeEventListener("pointercancel", onPointerCancel);
|
|
994
|
+
container.removeEventListener("lostpointercapture", onLostPointerCapture);
|
|
995
|
+
container.removeEventListener("contextmenu", handleContextMenu);
|
|
452
996
|
container.removeEventListener("wheel", handleWheel);
|
|
453
|
-
|
|
454
|
-
|
|
997
|
+
keyboardElement?.removeEventListener("keydown", onKeyDown);
|
|
998
|
+
keyboardElement?.removeEventListener("keyup", onKeyUp);
|
|
999
|
+
win?.removeEventListener("blur", onWindowBlur);
|
|
455
1000
|
};
|
|
456
1001
|
}
|
|
457
1002
|
//#endregion
|
|
458
|
-
export { applyFlowColorMode, attachContainer, attachInput, attachViewportTransform, computeRenderableIds, createMeasurementObserver, getAttachedContainer, nodePositionStyle, renderableIdsEqual, resolveFlowColorMode, viewportStyle };
|
|
1003
|
+
export { FLOW_CONTENT_MIME, applyFlowColorMode, attachContainer, attachInput, attachViewportTransform, canvasToClient, clientToCanvas, computeRenderableIds, createEntityGeometryObserver, createFlowClipboard, createFlowView, createMeasurementObserver, exportFlowImage, getAttachedContainer, nodePositionStyle, renderableIdsEqual, resolveFlowColorMode, viewportStyle };
|
package/package.json
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statelyai/flow-dom",
|
|
3
|
-
"version": "3.0
|
|
4
|
-
"description": "DOM-specific helpers for @statelyai/flow.",
|
|
3
|
+
"version": "3.2.0",
|
|
4
|
+
"description": "DOM-specific helpers for @statelyai/flow with core re-exports.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
5
7
|
"keywords": [
|
|
6
8
|
"flow",
|
|
7
9
|
"diagram",
|
|
@@ -24,25 +26,21 @@
|
|
|
24
26
|
}
|
|
25
27
|
},
|
|
26
28
|
"license": "MIT",
|
|
27
|
-
"
|
|
28
|
-
"@statelyai/flow": "
|
|
29
|
-
},
|
|
30
|
-
"peerDependenciesMeta": {
|
|
31
|
-
"@statelyai/flow": {
|
|
32
|
-
"optional": true
|
|
33
|
-
}
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@statelyai/flow": "0.5.0"
|
|
34
31
|
},
|
|
35
32
|
"devDependencies": {
|
|
33
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
36
34
|
"jsdom": "^25.0.0",
|
|
37
|
-
"tsdown": "^0.
|
|
35
|
+
"tsdown": "^0.22.13",
|
|
38
36
|
"typescript": "^5.8.0",
|
|
39
|
-
"vitest": "^4.1.0"
|
|
40
|
-
"@statelyai/flow": "0.4.2"
|
|
37
|
+
"vitest": "^4.1.0"
|
|
41
38
|
},
|
|
42
39
|
"scripts": {
|
|
43
40
|
"dev": "tsdown --watch",
|
|
44
41
|
"build": "tsdown",
|
|
45
42
|
"typecheck": "tsc --noEmit",
|
|
46
|
-
"test": "vitest run"
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:coverage": "vitest run --coverage"
|
|
47
45
|
}
|
|
48
46
|
}
|