@statelyai/flow-dom 3.1.0 → 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 +103 -6
- package/dist/esm/index.js +570 -26
- package/package.json +8 -4
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,8 +1,64 @@
|
|
|
1
|
-
import { FlowColorMode, FlowEngine, FlowInputEffects, FlowInputEvent, FlowStore, FlowStoreContext, Rect } from "@statelyai/flow";
|
|
1
|
+
import { EdgePathData, FlowColorMode, FlowController, FlowEngine, FlowEntityChangeNotifierOptions, FlowInputEffects, FlowInputEvent, FlowResult, FlowStore, FlowStoreContext, Point, Rect } from "@statelyai/flow";
|
|
2
2
|
export * from "@statelyai/flow";
|
|
3
|
-
|
|
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
|
|
4
58
|
//#region src/culling.d.ts
|
|
5
|
-
|
|
59
|
+
type ComputeRenderableIdsOptions = {
|
|
60
|
+
overscan?: number;
|
|
61
|
+
};
|
|
6
62
|
/**
|
|
7
63
|
* Node/edge ids worth mounting, or `null` meaning "render everything" — either
|
|
8
64
|
* because culling is disabled, the viewport hasn't been measured yet, or the
|
|
@@ -40,7 +96,7 @@ declare function renderableIdsEqual(a: RenderableIds, b: RenderableIds): boolean
|
|
|
40
96
|
* `getGraphBounds`/`getRenderableEntities` from the live store) — which holds
|
|
41
97
|
* when called inside a store-subscription selector.
|
|
42
98
|
*/
|
|
43
|
-
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;
|
|
44
100
|
//#endregion
|
|
45
101
|
//#region src/render-styles.d.ts
|
|
46
102
|
type NodePositionStyle = {
|
|
@@ -83,6 +139,34 @@ type ViewportStyle = {
|
|
|
83
139
|
*/
|
|
84
140
|
declare function viewportStyle(): ViewportStyle;
|
|
85
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
|
|
86
170
|
//#region src/index.d.ts
|
|
87
171
|
type ResolvedFlowColorMode = "light" | "dark";
|
|
88
172
|
declare function resolveFlowColorMode(colorMode: FlowColorMode, targetWindow?: Pick<Window, "matchMedia"> | undefined): ResolvedFlowColorMode;
|
|
@@ -99,17 +183,25 @@ type MeasurementObserver = {
|
|
|
99
183
|
observe(element: Element, target: MeasurementTarget): void;
|
|
100
184
|
/** Re-measure now (position-only changes don't fire ResizeObserver). */
|
|
101
185
|
measure(element: Element): void;
|
|
186
|
+
refresh(targets?: Iterable<string | Element>): void;
|
|
102
187
|
unobserve(element: Element): void;
|
|
103
188
|
disconnect(): void;
|
|
104
189
|
};
|
|
105
190
|
type MeasurementObserverOptions = {
|
|
106
191
|
container?: Element | (() => Element | null | undefined) | null;
|
|
192
|
+
equalityThreshold?: number;
|
|
107
193
|
};
|
|
108
194
|
declare function createMeasurementObserver(store: FlowStore, options?: MeasurementObserverOptions): MeasurementObserver;
|
|
109
195
|
/** The container element registered for this store via `attachContainer`. */
|
|
110
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;
|
|
111
199
|
declare function attachContainer(store: FlowStore, element: Element): () => void;
|
|
112
|
-
|
|
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;
|
|
113
205
|
type AttachInputOptions = {
|
|
114
206
|
container: HTMLElement;
|
|
115
207
|
store: FlowStore;
|
|
@@ -117,6 +209,11 @@ type AttachInputOptions = {
|
|
|
117
209
|
send(event: FlowInputEvent): FlowInputEffects;
|
|
118
210
|
cancelSession(): void;
|
|
119
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;
|
|
120
217
|
/**
|
|
121
218
|
* Auto-pan policy, evaluated once per animation frame while a gesture
|
|
122
219
|
* session holds the pointer near the container edge. Pass
|
|
@@ -135,4 +232,4 @@ type AttachInputOptions = {
|
|
|
135
232
|
};
|
|
136
233
|
declare function attachInput(options: AttachInputOptions): () => void;
|
|
137
234
|
//#endregion
|
|
138
|
-
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,4 +1,237 @@
|
|
|
1
|
+
import { createEntityChangeNotifier, pathToSVG } from "@statelyai/flow";
|
|
1
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
|
|
2
235
|
//#region src/culling.ts
|
|
3
236
|
/** Overscan (canvas px) added around the viewport so entities just off-screen
|
|
4
237
|
* stay mounted and don't pop in/out at the edges. */
|
|
@@ -37,20 +270,21 @@ function renderableIdsEqual(a, b) {
|
|
|
37
270
|
* `getGraphBounds`/`getRenderableEntities` from the live store) — which holds
|
|
38
271
|
* when called inside a store-subscription selector.
|
|
39
272
|
*/
|
|
40
|
-
function computeRenderableIds(engine, context, enabled) {
|
|
273
|
+
function computeRenderableIds(engine, context, enabled, options = {}) {
|
|
41
274
|
if (!enabled) return null;
|
|
275
|
+
const overscan = options.overscan ?? CULL_OVERSCAN;
|
|
42
276
|
const viewportSize = context.viewportSize;
|
|
43
277
|
if (viewportSize.width === 0 || viewportSize.height === 0) return null;
|
|
44
278
|
const viewport = context.viewport;
|
|
45
|
-
const left = -viewport.x / viewport.zoom -
|
|
46
|
-
const top = -viewport.y / viewport.zoom -
|
|
47
|
-
const right = left + viewportSize.width / viewport.zoom +
|
|
48
|
-
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;
|
|
49
283
|
const bounds = engine.getGraphBounds();
|
|
50
284
|
if (left <= bounds.x && top <= bounds.y && right >= bounds.x + bounds.width && bottom >= bounds.y + bounds.height) return null;
|
|
51
285
|
const renderable = engine.getRenderableEntities({
|
|
52
286
|
cull: "viewport",
|
|
53
|
-
overscan
|
|
287
|
+
overscan
|
|
54
288
|
});
|
|
55
289
|
return {
|
|
56
290
|
nodeIds: new Set(renderable.nodes.map((node) => node.id)),
|
|
@@ -98,6 +332,107 @@ function viewportStyle() {
|
|
|
98
332
|
};
|
|
99
333
|
}
|
|
100
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
|
|
101
436
|
//#region src/index.ts
|
|
102
437
|
function resolveFlowColorMode(colorMode, targetWindow = typeof window === "undefined" ? void 0 : window) {
|
|
103
438
|
if (colorMode !== "system") return colorMode;
|
|
@@ -112,6 +447,7 @@ function applyFlowColorMode(element, colorMode, targetWindow = element.ownerDocu
|
|
|
112
447
|
}
|
|
113
448
|
const containers = /* @__PURE__ */ new WeakMap();
|
|
114
449
|
function createMeasurementObserver(store, options = {}) {
|
|
450
|
+
const equalityThreshold = options.equalityThreshold ?? .5;
|
|
115
451
|
const elementMap = /* @__PURE__ */ new Map();
|
|
116
452
|
let pendingMeasurements = [];
|
|
117
453
|
let rafId = null;
|
|
@@ -138,14 +474,14 @@ function createMeasurementObserver(store, options = {}) {
|
|
|
138
474
|
};
|
|
139
475
|
const id = "portName" in target ? `${target.nodeId}:${target.portName}` : "nodeId" in target ? target.nodeId : target.edgeId;
|
|
140
476
|
const existing = store.getSnapshot().context.measurements.get(id);
|
|
141
|
-
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;
|
|
142
478
|
pendingMeasurements.push({
|
|
143
479
|
id,
|
|
144
480
|
rect: measurement
|
|
145
481
|
});
|
|
146
482
|
scheduleFlush();
|
|
147
483
|
}
|
|
148
|
-
const
|
|
484
|
+
const handleResize = (entries) => {
|
|
149
485
|
for (const entry of entries) {
|
|
150
486
|
const target = elementMap.get(entry.target);
|
|
151
487
|
if (!target) continue;
|
|
@@ -159,11 +495,17 @@ function createMeasurementObserver(store, options = {}) {
|
|
|
159
495
|
} : void 0;
|
|
160
496
|
pushMeasurement(entry.target, target, layoutSize);
|
|
161
497
|
}
|
|
162
|
-
}
|
|
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
|
+
}
|
|
163
505
|
return {
|
|
164
506
|
observe(element, target) {
|
|
165
507
|
elementMap.set(element, target);
|
|
166
|
-
|
|
508
|
+
getObserver(element).observe(element);
|
|
167
509
|
},
|
|
168
510
|
/**
|
|
169
511
|
* Re-measure an observed element immediately. ResizeObserver only fires
|
|
@@ -174,13 +516,22 @@ function createMeasurementObserver(store, options = {}) {
|
|
|
174
516
|
const target = elementMap.get(element);
|
|
175
517
|
if (target) pushMeasurement(element, target);
|
|
176
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
|
+
},
|
|
177
527
|
unobserve(element) {
|
|
178
528
|
elementMap.delete(element);
|
|
179
|
-
observer
|
|
529
|
+
observer?.unobserve(element);
|
|
180
530
|
},
|
|
181
531
|
disconnect() {
|
|
182
532
|
elementMap.clear();
|
|
183
|
-
observer
|
|
533
|
+
observer?.disconnect();
|
|
534
|
+
observer = null;
|
|
184
535
|
if (rafId !== null) {
|
|
185
536
|
cancelAnimationFrame(rafId);
|
|
186
537
|
rafId = null;
|
|
@@ -196,6 +547,23 @@ function getContainer(container) {
|
|
|
196
547
|
function getAttachedContainer(store) {
|
|
197
548
|
return containers.get(store);
|
|
198
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
|
+
}
|
|
199
567
|
function attachContainer(store, element) {
|
|
200
568
|
containers.set(store, element);
|
|
201
569
|
const update = () => {
|
|
@@ -206,21 +574,22 @@ function attachContainer(store, element) {
|
|
|
206
574
|
});
|
|
207
575
|
};
|
|
208
576
|
update();
|
|
209
|
-
const observer = new ResizeObserver(update);
|
|
577
|
+
const observer = new ((element.ownerDocument.defaultView?.ResizeObserver) ?? ResizeObserver)(update);
|
|
210
578
|
observer.observe(element);
|
|
211
579
|
return () => {
|
|
212
580
|
if (containers.get(store) === element) containers.delete(store);
|
|
213
581
|
observer.disconnect();
|
|
214
582
|
};
|
|
215
583
|
}
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
function attachViewportTransform(store, element) {
|
|
584
|
+
function attachViewportTransform(store, element, options = {}) {
|
|
585
|
+
const promoteIdleMs = options.promoteIdleMs ?? 200;
|
|
219
586
|
let prevViewport = store.getSnapshot().context.viewport;
|
|
220
587
|
let prevState = store.getSnapshot().context.canvasState;
|
|
221
588
|
let idle;
|
|
222
589
|
const writeTransform = (viewport) => {
|
|
223
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));
|
|
224
593
|
};
|
|
225
594
|
const promote = () => {
|
|
226
595
|
element.style.willChange = "transform";
|
|
@@ -230,7 +599,7 @@ function attachViewportTransform(store, element) {
|
|
|
230
599
|
if (idle !== void 0) clearTimeout(idle);
|
|
231
600
|
idle = setTimeout(() => {
|
|
232
601
|
element.style.willChange = "auto";
|
|
233
|
-
},
|
|
602
|
+
}, promoteIdleMs);
|
|
234
603
|
};
|
|
235
604
|
writeTransform(prevViewport);
|
|
236
605
|
if (prevState !== "idle") promote();
|
|
@@ -257,9 +626,20 @@ function attachInput(options) {
|
|
|
257
626
|
const { container, store, runtime } = options;
|
|
258
627
|
const doc = container.ownerDocument;
|
|
259
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;
|
|
260
633
|
let capturedPointerId = null;
|
|
261
634
|
let lastClient = null;
|
|
262
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;
|
|
263
643
|
function stopAutoPan() {
|
|
264
644
|
if (autoPanFrame !== null) {
|
|
265
645
|
win?.cancelAnimationFrame(autoPanFrame);
|
|
@@ -305,6 +685,18 @@ function attachInput(options) {
|
|
|
305
685
|
alt: event.altKey
|
|
306
686
|
};
|
|
307
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
|
+
}
|
|
308
700
|
function toPoints(event) {
|
|
309
701
|
const rect = container.getBoundingClientRect();
|
|
310
702
|
const screenPoint = {
|
|
@@ -322,24 +714,28 @@ function attachInput(options) {
|
|
|
322
714
|
};
|
|
323
715
|
}
|
|
324
716
|
function resolveTarget(event) {
|
|
325
|
-
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;
|
|
326
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" };
|
|
327
722
|
const portElement = element.closest("[data-flow-port-name]");
|
|
328
723
|
if (portElement?.dataset.flowPortName) return {
|
|
329
724
|
kind: "port",
|
|
330
725
|
nodeId: portElement.dataset.flowNodeId ?? "",
|
|
331
726
|
portName: portElement.dataset.flowPortName
|
|
332
727
|
};
|
|
333
|
-
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" };
|
|
334
729
|
const entityElement = element.closest("[data-entity-id]");
|
|
335
730
|
if (entityElement?.dataset.entityId) {
|
|
336
731
|
const isLabel = element.closest("[data-flow-edge-label]") !== null;
|
|
732
|
+
const part = element.closest("[data-flow-part]")?.dataset.flowPart ?? (isLabel ? "label" : void 0);
|
|
337
733
|
const entityHasDragHandle = entityElement.querySelector("[data-flow-drag-handle]") !== null;
|
|
338
734
|
const pressedDragHandle = entityHasDragHandle && element.closest("[data-flow-drag-handle]") !== null;
|
|
339
735
|
return {
|
|
340
736
|
kind: "entity",
|
|
341
737
|
id: entityElement.dataset.entityId,
|
|
342
|
-
...
|
|
738
|
+
...part ? { part } : {},
|
|
343
739
|
...entityHasDragHandle ? {
|
|
344
740
|
entityHasDragHandle,
|
|
345
741
|
pressedDragHandle
|
|
@@ -361,7 +757,9 @@ function attachInput(options) {
|
|
|
361
757
|
const effects = runtime.send({
|
|
362
758
|
kind,
|
|
363
759
|
pointerId: info.pointerId,
|
|
760
|
+
pointerType: info.pointerType ?? "mouse",
|
|
364
761
|
button: info.button,
|
|
762
|
+
detail: info.detail,
|
|
365
763
|
point,
|
|
366
764
|
screenPoint,
|
|
367
765
|
target: resolveTarget({
|
|
@@ -388,16 +786,116 @@ function attachInput(options) {
|
|
|
388
786
|
}
|
|
389
787
|
return effects;
|
|
390
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
|
+
}
|
|
391
842
|
function handlePointer(kind) {
|
|
392
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
|
+
}
|
|
393
888
|
if (kind === "pointer.down" || capturedPointerId !== null && event.pointerId === capturedPointerId) lastClient = {
|
|
394
889
|
x: event.clientX,
|
|
395
890
|
y: event.clientY,
|
|
396
891
|
pointerId: event.pointerId
|
|
397
892
|
};
|
|
893
|
+
const detail = kind === "pointer.down" && event.button === 0 ? computeClickDetail(event) : event.detail;
|
|
398
894
|
applyEffects(event, sendPointer(kind, event.clientX, event.clientY, {
|
|
399
895
|
pointerId: event.pointerId,
|
|
896
|
+
pointerType,
|
|
400
897
|
button: event.button,
|
|
898
|
+
detail,
|
|
401
899
|
modifiers: modifiers(event),
|
|
402
900
|
target: event.target
|
|
403
901
|
}));
|
|
@@ -416,10 +914,33 @@ function attachInput(options) {
|
|
|
416
914
|
modifiers: modifiers(event)
|
|
417
915
|
}));
|
|
418
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
|
+
}
|
|
419
927
|
function isEditableTarget(target) {
|
|
420
|
-
if (!(target instanceof
|
|
928
|
+
if (!HTMLElementCtor || !(target instanceof HTMLElementCtor)) return false;
|
|
421
929
|
return target.isContentEditable || target.matches("input, textarea, select, [contenteditable='true']");
|
|
422
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
|
+
}
|
|
423
944
|
function handleKey(kind) {
|
|
424
945
|
return (event) => {
|
|
425
946
|
applyEffects(event, runtime.send({
|
|
@@ -434,26 +955,49 @@ function attachInput(options) {
|
|
|
434
955
|
const onPointerMove = handlePointer("pointer.move");
|
|
435
956
|
const onPointerUp = handlePointer("pointer.up");
|
|
436
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
|
+
};
|
|
437
971
|
const onKeyDown = handleKey("key.down");
|
|
438
972
|
const onKeyUp = handleKey("key.up");
|
|
973
|
+
if (keyboardTarget === "container" && !container.hasAttribute("tabindex")) container.tabIndex = 0;
|
|
439
974
|
container.addEventListener("pointerdown", onPointerDown);
|
|
440
975
|
container.addEventListener("pointermove", onPointerMove);
|
|
441
976
|
container.addEventListener("pointerup", onPointerUp);
|
|
442
977
|
container.addEventListener("pointercancel", onPointerCancel);
|
|
978
|
+
container.addEventListener("lostpointercapture", onLostPointerCapture);
|
|
979
|
+
container.addEventListener("contextmenu", handleContextMenu);
|
|
443
980
|
container.addEventListener("wheel", handleWheel, { passive: false });
|
|
444
|
-
|
|
445
|
-
|
|
981
|
+
keyboardElement?.addEventListener("keydown", onKeyDown);
|
|
982
|
+
keyboardElement?.addEventListener("keyup", onKeyUp);
|
|
983
|
+
win?.addEventListener("blur", onWindowBlur);
|
|
446
984
|
return () => {
|
|
447
985
|
stopAutoPan();
|
|
448
986
|
runtime.cancelSession();
|
|
987
|
+
activeTouchPointers.clear();
|
|
988
|
+
pinchActive = false;
|
|
989
|
+
lastPinch = null;
|
|
449
990
|
container.removeEventListener("pointerdown", onPointerDown);
|
|
450
991
|
container.removeEventListener("pointermove", onPointerMove);
|
|
451
992
|
container.removeEventListener("pointerup", onPointerUp);
|
|
452
993
|
container.removeEventListener("pointercancel", onPointerCancel);
|
|
994
|
+
container.removeEventListener("lostpointercapture", onLostPointerCapture);
|
|
995
|
+
container.removeEventListener("contextmenu", handleContextMenu);
|
|
453
996
|
container.removeEventListener("wheel", handleWheel);
|
|
454
|
-
|
|
455
|
-
|
|
997
|
+
keyboardElement?.removeEventListener("keydown", onKeyDown);
|
|
998
|
+
keyboardElement?.removeEventListener("keyup", onKeyUp);
|
|
999
|
+
win?.removeEventListener("blur", onWindowBlur);
|
|
456
1000
|
};
|
|
457
1001
|
}
|
|
458
1002
|
//#endregion
|
|
459
|
-
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.
|
|
3
|
+
"version": "3.2.0",
|
|
4
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",
|
|
@@ -25,11 +27,12 @@
|
|
|
25
27
|
},
|
|
26
28
|
"license": "MIT",
|
|
27
29
|
"dependencies": {
|
|
28
|
-
"@statelyai/flow": "0.
|
|
30
|
+
"@statelyai/flow": "0.5.0"
|
|
29
31
|
},
|
|
30
32
|
"devDependencies": {
|
|
33
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
31
34
|
"jsdom": "^25.0.0",
|
|
32
|
-
"tsdown": "^0.
|
|
35
|
+
"tsdown": "^0.22.13",
|
|
33
36
|
"typescript": "^5.8.0",
|
|
34
37
|
"vitest": "^4.1.0"
|
|
35
38
|
},
|
|
@@ -37,6 +40,7 @@
|
|
|
37
40
|
"dev": "tsdown --watch",
|
|
38
41
|
"build": "tsdown",
|
|
39
42
|
"typecheck": "tsc --noEmit",
|
|
40
|
-
"test": "vitest run"
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:coverage": "vitest run --coverage"
|
|
41
45
|
}
|
|
42
46
|
}
|