@liveblocks/react-flow 0.0.0 → 3.16.0-flow1
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/README.md +6 -0
- package/dist/cursors.cjs +148 -0
- package/dist/cursors.cjs.map +1 -0
- package/dist/cursors.js +146 -0
- package/dist/cursors.js.map +1 -0
- package/dist/flow.cjs +332 -0
- package/dist/flow.cjs.map +1 -0
- package/dist/flow.js +328 -0
- package/dist/flow.js.map +1 -0
- package/dist/index.cjs +13 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +150 -0
- package/dist/index.d.ts +150 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/suspense.cjs +13 -0
- package/dist/suspense.cjs.map +1 -0
- package/dist/suspense.d.cts +149 -0
- package/dist/suspense.d.ts +149 -0
- package/dist/suspense.js +7 -0
- package/dist/suspense.js.map +1 -0
- package/dist/version.cjs +10 -0
- package/dist/version.cjs.map +1 -0
- package/dist/version.js +6 -0
- package/dist/version.js.map +1 -0
- package/package.json +116 -2
- package/src/styles/index.css +12 -0
- package/styles.css +1 -0
- package/styles.css.d.cts +1 -0
- package/styles.css.d.ts +1 -0
- package/styles.css.map +1 -0
- package/suspense/README.md +5 -0
- package/suspense/package.json +4 -0
- package/index.js +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<p>
|
|
2
|
+
<a href="https://liveblocks.io#gh-light-mode-only"><img src="https://raw.githubusercontent.com/liveblocks/liveblocks/main/.github/assets/header-light.svg" alt="Liveblocks" /></a>
|
|
3
|
+
<a href="https://liveblocks.io#gh-dark-mode-only"><img src="https://raw.githubusercontent.com/liveblocks/liveblocks/main/.github/assets/header-dark.svg" alt="Liveblocks" /></a>
|
|
4
|
+
</p>
|
|
5
|
+
|
|
6
|
+
# `@liveblocks/react-flow`
|
package/dist/cursors.cjs
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
4
|
+
var core = require('@liveblocks/core');
|
|
5
|
+
var react = require('@liveblocks/react');
|
|
6
|
+
var _private = require('@liveblocks/react/_private');
|
|
7
|
+
var reactUi = require('@liveblocks/react-ui');
|
|
8
|
+
var _private$1 = require('@liveblocks/react-ui/_private');
|
|
9
|
+
var react$2 = require('@xyflow/react');
|
|
10
|
+
var react$1 = require('react');
|
|
11
|
+
|
|
12
|
+
const DEFAULT_PRESENCE_KEY = "cursor";
|
|
13
|
+
function $string(value) {
|
|
14
|
+
return typeof value === "string" ? value : void 0;
|
|
15
|
+
}
|
|
16
|
+
function $coordinates(value) {
|
|
17
|
+
if (core.isPlainObject(value) && typeof value.x === "number" && typeof value.y === "number") {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
return void 0;
|
|
21
|
+
}
|
|
22
|
+
function PresenceCursor({
|
|
23
|
+
connectionId,
|
|
24
|
+
presenceKey
|
|
25
|
+
}) {
|
|
26
|
+
const room = react.useRoom();
|
|
27
|
+
const cursorRef = react$1.useRef(null);
|
|
28
|
+
const reactFlowStoreApi = react$2.useStoreApi();
|
|
29
|
+
const userId = react.useOther(connectionId, (other) => $string(other.id));
|
|
30
|
+
const { user, isLoading } = react.useUser(userId ?? "");
|
|
31
|
+
const hasUserInfo = userId !== void 0 && !isLoading;
|
|
32
|
+
const color = $string(user?.color);
|
|
33
|
+
const name = $string(user?.name);
|
|
34
|
+
_private.useLayoutEffect(() => {
|
|
35
|
+
const spring = _private$1.makeCursorSpring();
|
|
36
|
+
function update() {
|
|
37
|
+
const element = cursorRef.current;
|
|
38
|
+
const coordinates = spring.get();
|
|
39
|
+
const [panX, panY, zoom] = reactFlowStoreApi.getState().transform;
|
|
40
|
+
if (!element) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!hasUserInfo || coordinates === null) {
|
|
44
|
+
element.style.transform = "translate3d(0, 0, 0)";
|
|
45
|
+
element.style.display = "none";
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
element.style.transform = `translate3d(${coordinates.x * zoom + panX}px, ${coordinates.y * zoom + panY}px, 0)`;
|
|
49
|
+
element.style.display = "";
|
|
50
|
+
}
|
|
51
|
+
update();
|
|
52
|
+
const unsubscribeSpring = spring.subscribe(update);
|
|
53
|
+
const unsubscribeTransform = reactFlowStoreApi.subscribe(
|
|
54
|
+
(state, previousState) => {
|
|
55
|
+
if (state.transform !== previousState.transform) {
|
|
56
|
+
update();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
const unsubscribeOther = room.events.others.subscribe(({ others }) => {
|
|
61
|
+
const other = others.find((other2) => other2.connectionId === connectionId);
|
|
62
|
+
const cursor = $coordinates(other?.presence[presenceKey]);
|
|
63
|
+
spring.set(cursor ?? null);
|
|
64
|
+
});
|
|
65
|
+
return () => {
|
|
66
|
+
spring.dispose();
|
|
67
|
+
unsubscribeSpring();
|
|
68
|
+
unsubscribeTransform();
|
|
69
|
+
unsubscribeOther();
|
|
70
|
+
};
|
|
71
|
+
}, [room, connectionId, presenceKey, hasUserInfo, reactFlowStoreApi]);
|
|
72
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
73
|
+
reactUi.Cursor,
|
|
74
|
+
{
|
|
75
|
+
color,
|
|
76
|
+
label: name,
|
|
77
|
+
ref: cursorRef,
|
|
78
|
+
style: { display: "none" }
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const Cursors = react$1.forwardRef(
|
|
83
|
+
({ className, children, presenceKey = DEFAULT_PRESENCE_KEY, ...props }, forwardedRef) => {
|
|
84
|
+
const reactFlow = react$2.useReactFlow();
|
|
85
|
+
const updateMyPresence = react.useUpdateMyPresence();
|
|
86
|
+
const othersConnectionIds = react.useOthersConnectionIds();
|
|
87
|
+
const reactFlowDomNode = react$2.useStore((state) => state.domNode);
|
|
88
|
+
const reactFlowStoreApi = react$2.useStoreApi();
|
|
89
|
+
const handlePointerMove = react$1.useCallback(
|
|
90
|
+
(event) => {
|
|
91
|
+
const isPanning = reactFlowStoreApi.getState().paneDragging;
|
|
92
|
+
if (isPanning) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
updateMyPresence({
|
|
96
|
+
[presenceKey]: reactFlow.screenToFlowPosition({
|
|
97
|
+
x: event.clientX,
|
|
98
|
+
y: event.clientY
|
|
99
|
+
})
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
[updateMyPresence, presenceKey, reactFlow, reactFlowStoreApi]
|
|
103
|
+
);
|
|
104
|
+
const handlePointerLeave = react$1.useCallback(() => {
|
|
105
|
+
updateMyPresence({ [presenceKey]: null });
|
|
106
|
+
}, [updateMyPresence, presenceKey]);
|
|
107
|
+
react$1.useEffect(() => {
|
|
108
|
+
if (!reactFlowDomNode) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
reactFlowDomNode.addEventListener("pointermove", handlePointerMove);
|
|
112
|
+
reactFlowDomNode.addEventListener("pointerleave", handlePointerLeave);
|
|
113
|
+
window.addEventListener("blur", handlePointerLeave);
|
|
114
|
+
return () => {
|
|
115
|
+
reactFlowDomNode.removeEventListener("pointermove", handlePointerMove);
|
|
116
|
+
reactFlowDomNode.removeEventListener(
|
|
117
|
+
"pointerleave",
|
|
118
|
+
handlePointerLeave
|
|
119
|
+
);
|
|
120
|
+
window.removeEventListener("blur", handlePointerLeave);
|
|
121
|
+
handlePointerLeave();
|
|
122
|
+
};
|
|
123
|
+
}, [reactFlowDomNode, handlePointerMove, handlePointerLeave]);
|
|
124
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
125
|
+
"div",
|
|
126
|
+
{
|
|
127
|
+
"aria-hidden": true,
|
|
128
|
+
className: _private$1.cn("lb-root lb-react-flow-cursors", className),
|
|
129
|
+
...props,
|
|
130
|
+
ref: forwardedRef,
|
|
131
|
+
children: [
|
|
132
|
+
othersConnectionIds.map((connectionId) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
133
|
+
PresenceCursor,
|
|
134
|
+
{
|
|
135
|
+
connectionId,
|
|
136
|
+
presenceKey
|
|
137
|
+
},
|
|
138
|
+
connectionId
|
|
139
|
+
)),
|
|
140
|
+
children
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
exports.Cursors = Cursors;
|
|
148
|
+
//# sourceMappingURL=cursors.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursors.cjs","sources":["../src/cursors.tsx"],"sourcesContent":["import { isPlainObject } from \"@liveblocks/core\";\nimport {\n useOther,\n useOthersConnectionIds,\n useRoom,\n useUpdateMyPresence,\n useUser,\n} from \"@liveblocks/react\";\nimport { useLayoutEffect } from \"@liveblocks/react/_private\";\nimport { Cursor } from \"@liveblocks/react-ui\";\nimport { cn, makeCursorSpring } from \"@liveblocks/react-ui/_private\";\nimport { useReactFlow, useStore, useStoreApi } from \"@xyflow/react\";\nimport type { ComponentPropsWithoutRef } from \"react\";\nimport { forwardRef, useCallback, useEffect, useRef } from \"react\";\n\nconst DEFAULT_PRESENCE_KEY = \"cursor\";\n\nexport interface CursorsProps extends ComponentPropsWithoutRef<\"div\"> {\n /**\n * The key used to store the cursors in users' Presence.\n *\n * Defaults to `\"cursor\"`.\n */\n presenceKey?: string;\n}\n\ntype Coordinates = {\n x: number;\n y: number;\n};\n\nfunction $string(value: unknown): string | undefined {\n return typeof value === \"string\" ? value : undefined;\n}\n\nfunction $coordinates(value: unknown): Coordinates | undefined {\n if (\n isPlainObject(value) &&\n typeof value.x === \"number\" &&\n typeof value.y === \"number\"\n ) {\n return value as Coordinates;\n }\n\n return undefined;\n}\n\nfunction PresenceCursor({\n connectionId,\n presenceKey,\n}: {\n connectionId: number;\n presenceKey: string;\n}) {\n const room = useRoom();\n const cursorRef = useRef<HTMLDivElement>(null);\n const reactFlowStoreApi = useStoreApi();\n const userId = useOther(connectionId, (other) => $string(other.id));\n const { user, isLoading } = useUser(userId ?? \"\");\n const hasUserInfo = userId !== undefined && !isLoading;\n const color = $string(user?.color);\n const name = $string(user?.name);\n\n useLayoutEffect(() => {\n const spring = makeCursorSpring();\n\n function update() {\n const element = cursorRef.current;\n const coordinates = spring.get();\n const [panX, panY, zoom] = reactFlowStoreApi.getState().transform;\n\n if (!element) {\n return;\n }\n\n if (!hasUserInfo || coordinates === null) {\n element.style.transform = \"translate3d(0, 0, 0)\";\n element.style.display = \"none\";\n return;\n }\n\n element.style.transform = `translate3d(${coordinates.x * zoom + panX}px, ${coordinates.y * zoom + panY}px, 0)`;\n element.style.display = \"\";\n }\n\n update();\n\n const unsubscribeSpring = spring.subscribe(update);\n const unsubscribeTransform = reactFlowStoreApi.subscribe(\n (state, previousState) => {\n // Update positions whenever the canvas is panned or zoomed.\n if (state.transform !== previousState.transform) {\n update();\n }\n }\n );\n const unsubscribeOther = room.events.others.subscribe(({ others }) => {\n const other = others.find((other) => other.connectionId === connectionId);\n const cursor = $coordinates(other?.presence[presenceKey]);\n\n spring.set(cursor ?? null);\n });\n\n return () => {\n spring.dispose();\n unsubscribeSpring();\n unsubscribeTransform();\n unsubscribeOther();\n };\n }, [room, connectionId, presenceKey, hasUserInfo, reactFlowStoreApi]);\n\n return (\n <Cursor\n color={color}\n label={name}\n ref={cursorRef}\n style={{ display: \"none\" }}\n />\n );\n}\n\n/**\n * Displays other users' cursors inside a React Flow canvas and stores the\n * current user's cursor in Presence as `{ cursor: { x, y } }`.\n *\n * Cursor coordinates are kept in React Flow canvas space, so panning moves\n * them correctly while zooming only affects their position, not their size.\n */\nexport const Cursors = forwardRef<HTMLDivElement, CursorsProps>(\n (\n { className, children, presenceKey = DEFAULT_PRESENCE_KEY, ...props },\n forwardedRef\n ) => {\n const reactFlow = useReactFlow();\n const updateMyPresence = useUpdateMyPresence();\n const othersConnectionIds = useOthersConnectionIds();\n const reactFlowDomNode = useStore((state) => state.domNode);\n const reactFlowStoreApi = useStoreApi();\n\n const handlePointerMove = useCallback(\n (event: PointerEvent) => {\n const isPanning = reactFlowStoreApi.getState().paneDragging;\n\n if (isPanning) {\n return;\n }\n\n updateMyPresence({\n [presenceKey]: reactFlow.screenToFlowPosition({\n x: event.clientX,\n y: event.clientY,\n }),\n });\n },\n [updateMyPresence, presenceKey, reactFlow, reactFlowStoreApi]\n );\n\n const handlePointerLeave = useCallback(() => {\n updateMyPresence({ [presenceKey]: null });\n }, [updateMyPresence, presenceKey]);\n\n useEffect(() => {\n if (!reactFlowDomNode) {\n return;\n }\n\n reactFlowDomNode.addEventListener(\"pointermove\", handlePointerMove);\n reactFlowDomNode.addEventListener(\"pointerleave\", handlePointerLeave);\n window.addEventListener(\"blur\", handlePointerLeave);\n\n return () => {\n reactFlowDomNode.removeEventListener(\"pointermove\", handlePointerMove);\n reactFlowDomNode.removeEventListener(\n \"pointerleave\",\n handlePointerLeave\n );\n window.removeEventListener(\"blur\", handlePointerLeave);\n handlePointerLeave();\n };\n }, [reactFlowDomNode, handlePointerMove, handlePointerLeave]);\n\n return (\n <div\n aria-hidden\n className={cn(\"lb-root lb-react-flow-cursors\", className)}\n {...props}\n ref={forwardedRef}\n >\n {othersConnectionIds.map((connectionId) => (\n <PresenceCursor\n key={connectionId}\n connectionId={connectionId}\n presenceKey={presenceKey}\n />\n ))}\n\n {children}\n </div>\n );\n }\n);\n"],"names":["isPlainObject","useRoom","useRef","useStoreApi","useOther","useUser","useLayoutEffect","makeCursorSpring","other","jsx","Cursor","forwardRef","useReactFlow","useUpdateMyPresence","useOthersConnectionIds","useStore","useCallback","useEffect","jsxs","cn"],"mappings":"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"}
|
package/dist/cursors.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
+
import { isPlainObject } from '@liveblocks/core';
|
|
3
|
+
import { useRoom, useOther, useUser, useUpdateMyPresence, useOthersConnectionIds } from '@liveblocks/react';
|
|
4
|
+
import { useLayoutEffect } from '@liveblocks/react/_private';
|
|
5
|
+
import { Cursor } from '@liveblocks/react-ui';
|
|
6
|
+
import { makeCursorSpring, cn } from '@liveblocks/react-ui/_private';
|
|
7
|
+
import { useStoreApi, useReactFlow, useStore } from '@xyflow/react';
|
|
8
|
+
import { useRef, forwardRef, useCallback, useEffect } from 'react';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_PRESENCE_KEY = "cursor";
|
|
11
|
+
function $string(value) {
|
|
12
|
+
return typeof value === "string" ? value : void 0;
|
|
13
|
+
}
|
|
14
|
+
function $coordinates(value) {
|
|
15
|
+
if (isPlainObject(value) && typeof value.x === "number" && typeof value.y === "number") {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
return void 0;
|
|
19
|
+
}
|
|
20
|
+
function PresenceCursor({
|
|
21
|
+
connectionId,
|
|
22
|
+
presenceKey
|
|
23
|
+
}) {
|
|
24
|
+
const room = useRoom();
|
|
25
|
+
const cursorRef = useRef(null);
|
|
26
|
+
const reactFlowStoreApi = useStoreApi();
|
|
27
|
+
const userId = useOther(connectionId, (other) => $string(other.id));
|
|
28
|
+
const { user, isLoading } = useUser(userId ?? "");
|
|
29
|
+
const hasUserInfo = userId !== void 0 && !isLoading;
|
|
30
|
+
const color = $string(user?.color);
|
|
31
|
+
const name = $string(user?.name);
|
|
32
|
+
useLayoutEffect(() => {
|
|
33
|
+
const spring = makeCursorSpring();
|
|
34
|
+
function update() {
|
|
35
|
+
const element = cursorRef.current;
|
|
36
|
+
const coordinates = spring.get();
|
|
37
|
+
const [panX, panY, zoom] = reactFlowStoreApi.getState().transform;
|
|
38
|
+
if (!element) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (!hasUserInfo || coordinates === null) {
|
|
42
|
+
element.style.transform = "translate3d(0, 0, 0)";
|
|
43
|
+
element.style.display = "none";
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
element.style.transform = `translate3d(${coordinates.x * zoom + panX}px, ${coordinates.y * zoom + panY}px, 0)`;
|
|
47
|
+
element.style.display = "";
|
|
48
|
+
}
|
|
49
|
+
update();
|
|
50
|
+
const unsubscribeSpring = spring.subscribe(update);
|
|
51
|
+
const unsubscribeTransform = reactFlowStoreApi.subscribe(
|
|
52
|
+
(state, previousState) => {
|
|
53
|
+
if (state.transform !== previousState.transform) {
|
|
54
|
+
update();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
const unsubscribeOther = room.events.others.subscribe(({ others }) => {
|
|
59
|
+
const other = others.find((other2) => other2.connectionId === connectionId);
|
|
60
|
+
const cursor = $coordinates(other?.presence[presenceKey]);
|
|
61
|
+
spring.set(cursor ?? null);
|
|
62
|
+
});
|
|
63
|
+
return () => {
|
|
64
|
+
spring.dispose();
|
|
65
|
+
unsubscribeSpring();
|
|
66
|
+
unsubscribeTransform();
|
|
67
|
+
unsubscribeOther();
|
|
68
|
+
};
|
|
69
|
+
}, [room, connectionId, presenceKey, hasUserInfo, reactFlowStoreApi]);
|
|
70
|
+
return /* @__PURE__ */ jsx(
|
|
71
|
+
Cursor,
|
|
72
|
+
{
|
|
73
|
+
color,
|
|
74
|
+
label: name,
|
|
75
|
+
ref: cursorRef,
|
|
76
|
+
style: { display: "none" }
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const Cursors = forwardRef(
|
|
81
|
+
({ className, children, presenceKey = DEFAULT_PRESENCE_KEY, ...props }, forwardedRef) => {
|
|
82
|
+
const reactFlow = useReactFlow();
|
|
83
|
+
const updateMyPresence = useUpdateMyPresence();
|
|
84
|
+
const othersConnectionIds = useOthersConnectionIds();
|
|
85
|
+
const reactFlowDomNode = useStore((state) => state.domNode);
|
|
86
|
+
const reactFlowStoreApi = useStoreApi();
|
|
87
|
+
const handlePointerMove = useCallback(
|
|
88
|
+
(event) => {
|
|
89
|
+
const isPanning = reactFlowStoreApi.getState().paneDragging;
|
|
90
|
+
if (isPanning) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
updateMyPresence({
|
|
94
|
+
[presenceKey]: reactFlow.screenToFlowPosition({
|
|
95
|
+
x: event.clientX,
|
|
96
|
+
y: event.clientY
|
|
97
|
+
})
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
[updateMyPresence, presenceKey, reactFlow, reactFlowStoreApi]
|
|
101
|
+
);
|
|
102
|
+
const handlePointerLeave = useCallback(() => {
|
|
103
|
+
updateMyPresence({ [presenceKey]: null });
|
|
104
|
+
}, [updateMyPresence, presenceKey]);
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!reactFlowDomNode) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
reactFlowDomNode.addEventListener("pointermove", handlePointerMove);
|
|
110
|
+
reactFlowDomNode.addEventListener("pointerleave", handlePointerLeave);
|
|
111
|
+
window.addEventListener("blur", handlePointerLeave);
|
|
112
|
+
return () => {
|
|
113
|
+
reactFlowDomNode.removeEventListener("pointermove", handlePointerMove);
|
|
114
|
+
reactFlowDomNode.removeEventListener(
|
|
115
|
+
"pointerleave",
|
|
116
|
+
handlePointerLeave
|
|
117
|
+
);
|
|
118
|
+
window.removeEventListener("blur", handlePointerLeave);
|
|
119
|
+
handlePointerLeave();
|
|
120
|
+
};
|
|
121
|
+
}, [reactFlowDomNode, handlePointerMove, handlePointerLeave]);
|
|
122
|
+
return /* @__PURE__ */ jsxs(
|
|
123
|
+
"div",
|
|
124
|
+
{
|
|
125
|
+
"aria-hidden": true,
|
|
126
|
+
className: cn("lb-root lb-react-flow-cursors", className),
|
|
127
|
+
...props,
|
|
128
|
+
ref: forwardedRef,
|
|
129
|
+
children: [
|
|
130
|
+
othersConnectionIds.map((connectionId) => /* @__PURE__ */ jsx(
|
|
131
|
+
PresenceCursor,
|
|
132
|
+
{
|
|
133
|
+
connectionId,
|
|
134
|
+
presenceKey
|
|
135
|
+
},
|
|
136
|
+
connectionId
|
|
137
|
+
)),
|
|
138
|
+
children
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
export { Cursors };
|
|
146
|
+
//# sourceMappingURL=cursors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursors.js","sources":["../src/cursors.tsx"],"sourcesContent":["import { isPlainObject } from \"@liveblocks/core\";\nimport {\n useOther,\n useOthersConnectionIds,\n useRoom,\n useUpdateMyPresence,\n useUser,\n} from \"@liveblocks/react\";\nimport { useLayoutEffect } from \"@liveblocks/react/_private\";\nimport { Cursor } from \"@liveblocks/react-ui\";\nimport { cn, makeCursorSpring } from \"@liveblocks/react-ui/_private\";\nimport { useReactFlow, useStore, useStoreApi } from \"@xyflow/react\";\nimport type { ComponentPropsWithoutRef } from \"react\";\nimport { forwardRef, useCallback, useEffect, useRef } from \"react\";\n\nconst DEFAULT_PRESENCE_KEY = \"cursor\";\n\nexport interface CursorsProps extends ComponentPropsWithoutRef<\"div\"> {\n /**\n * The key used to store the cursors in users' Presence.\n *\n * Defaults to `\"cursor\"`.\n */\n presenceKey?: string;\n}\n\ntype Coordinates = {\n x: number;\n y: number;\n};\n\nfunction $string(value: unknown): string | undefined {\n return typeof value === \"string\" ? value : undefined;\n}\n\nfunction $coordinates(value: unknown): Coordinates | undefined {\n if (\n isPlainObject(value) &&\n typeof value.x === \"number\" &&\n typeof value.y === \"number\"\n ) {\n return value as Coordinates;\n }\n\n return undefined;\n}\n\nfunction PresenceCursor({\n connectionId,\n presenceKey,\n}: {\n connectionId: number;\n presenceKey: string;\n}) {\n const room = useRoom();\n const cursorRef = useRef<HTMLDivElement>(null);\n const reactFlowStoreApi = useStoreApi();\n const userId = useOther(connectionId, (other) => $string(other.id));\n const { user, isLoading } = useUser(userId ?? \"\");\n const hasUserInfo = userId !== undefined && !isLoading;\n const color = $string(user?.color);\n const name = $string(user?.name);\n\n useLayoutEffect(() => {\n const spring = makeCursorSpring();\n\n function update() {\n const element = cursorRef.current;\n const coordinates = spring.get();\n const [panX, panY, zoom] = reactFlowStoreApi.getState().transform;\n\n if (!element) {\n return;\n }\n\n if (!hasUserInfo || coordinates === null) {\n element.style.transform = \"translate3d(0, 0, 0)\";\n element.style.display = \"none\";\n return;\n }\n\n element.style.transform = `translate3d(${coordinates.x * zoom + panX}px, ${coordinates.y * zoom + panY}px, 0)`;\n element.style.display = \"\";\n }\n\n update();\n\n const unsubscribeSpring = spring.subscribe(update);\n const unsubscribeTransform = reactFlowStoreApi.subscribe(\n (state, previousState) => {\n // Update positions whenever the canvas is panned or zoomed.\n if (state.transform !== previousState.transform) {\n update();\n }\n }\n );\n const unsubscribeOther = room.events.others.subscribe(({ others }) => {\n const other = others.find((other) => other.connectionId === connectionId);\n const cursor = $coordinates(other?.presence[presenceKey]);\n\n spring.set(cursor ?? null);\n });\n\n return () => {\n spring.dispose();\n unsubscribeSpring();\n unsubscribeTransform();\n unsubscribeOther();\n };\n }, [room, connectionId, presenceKey, hasUserInfo, reactFlowStoreApi]);\n\n return (\n <Cursor\n color={color}\n label={name}\n ref={cursorRef}\n style={{ display: \"none\" }}\n />\n );\n}\n\n/**\n * Displays other users' cursors inside a React Flow canvas and stores the\n * current user's cursor in Presence as `{ cursor: { x, y } }`.\n *\n * Cursor coordinates are kept in React Flow canvas space, so panning moves\n * them correctly while zooming only affects their position, not their size.\n */\nexport const Cursors = forwardRef<HTMLDivElement, CursorsProps>(\n (\n { className, children, presenceKey = DEFAULT_PRESENCE_KEY, ...props },\n forwardedRef\n ) => {\n const reactFlow = useReactFlow();\n const updateMyPresence = useUpdateMyPresence();\n const othersConnectionIds = useOthersConnectionIds();\n const reactFlowDomNode = useStore((state) => state.domNode);\n const reactFlowStoreApi = useStoreApi();\n\n const handlePointerMove = useCallback(\n (event: PointerEvent) => {\n const isPanning = reactFlowStoreApi.getState().paneDragging;\n\n if (isPanning) {\n return;\n }\n\n updateMyPresence({\n [presenceKey]: reactFlow.screenToFlowPosition({\n x: event.clientX,\n y: event.clientY,\n }),\n });\n },\n [updateMyPresence, presenceKey, reactFlow, reactFlowStoreApi]\n );\n\n const handlePointerLeave = useCallback(() => {\n updateMyPresence({ [presenceKey]: null });\n }, [updateMyPresence, presenceKey]);\n\n useEffect(() => {\n if (!reactFlowDomNode) {\n return;\n }\n\n reactFlowDomNode.addEventListener(\"pointermove\", handlePointerMove);\n reactFlowDomNode.addEventListener(\"pointerleave\", handlePointerLeave);\n window.addEventListener(\"blur\", handlePointerLeave);\n\n return () => {\n reactFlowDomNode.removeEventListener(\"pointermove\", handlePointerMove);\n reactFlowDomNode.removeEventListener(\n \"pointerleave\",\n handlePointerLeave\n );\n window.removeEventListener(\"blur\", handlePointerLeave);\n handlePointerLeave();\n };\n }, [reactFlowDomNode, handlePointerMove, handlePointerLeave]);\n\n return (\n <div\n aria-hidden\n className={cn(\"lb-root lb-react-flow-cursors\", className)}\n {...props}\n ref={forwardedRef}\n >\n {othersConnectionIds.map((connectionId) => (\n <PresenceCursor\n key={connectionId}\n connectionId={connectionId}\n presenceKey={presenceKey}\n />\n ))}\n\n {children}\n </div>\n );\n }\n);\n"],"names":["other"],"mappings":";;;;;;;;;AAeA,MAAM,oBAAuB,GAAA,QAAA,CAAA;AAgB7B,SAAS,QAAQ,KAAoC,EAAA;AACnD,EAAO,OAAA,OAAO,KAAU,KAAA,QAAA,GAAW,KAAQ,GAAA,KAAA,CAAA,CAAA;AAC7C,CAAA;AAEA,SAAS,aAAa,KAAyC,EAAA;AAC7D,EACE,IAAA,aAAA,CAAc,KAAK,CAAA,IACnB,OAAO,KAAA,CAAM,MAAM,QACnB,IAAA,OAAO,KAAM,CAAA,CAAA,KAAM,QACnB,EAAA;AACA,IAAO,OAAA,KAAA,CAAA;AAAA,GACT;AAEA,EAAO,OAAA,KAAA,CAAA,CAAA;AACT,CAAA;AAEA,SAAS,cAAe,CAAA;AAAA,EACtB,YAAA;AAAA,EACA,WAAA;AACF,CAGG,EAAA;AACD,EAAA,MAAM,OAAO,OAAQ,EAAA,CAAA;AACrB,EAAM,MAAA,SAAA,GAAY,OAAuB,IAAI,CAAA,CAAA;AAC7C,EAAA,MAAM,oBAAoB,WAAY,EAAA,CAAA;AACtC,EAAM,MAAA,MAAA,GAAS,SAAS,YAAc,EAAA,CAAC,UAAU,OAAQ,CAAA,KAAA,CAAM,EAAE,CAAC,CAAA,CAAA;AAClE,EAAA,MAAM,EAAE,IAAM,EAAA,SAAA,EAAc,GAAA,OAAA,CAAQ,UAAU,EAAE,CAAA,CAAA;AAChD,EAAM,MAAA,WAAA,GAAc,MAAW,KAAA,KAAA,CAAA,IAAa,CAAC,SAAA,CAAA;AAC7C,EAAM,MAAA,KAAA,GAAQ,OAAQ,CAAA,IAAA,EAAM,KAAK,CAAA,CAAA;AACjC,EAAM,MAAA,IAAA,GAAO,OAAQ,CAAA,IAAA,EAAM,IAAI,CAAA,CAAA;AAE/B,EAAA,eAAA,CAAgB,MAAM;AACpB,IAAA,MAAM,SAAS,gBAAiB,EAAA,CAAA;AAEhC,IAAA,SAAS,MAAS,GAAA;AAChB,MAAA,MAAM,UAAU,SAAU,CAAA,OAAA,CAAA;AAC1B,MAAM,MAAA,WAAA,GAAc,OAAO,GAAI,EAAA,CAAA;AAC/B,MAAA,MAAM,CAAC,IAAM,EAAA,IAAA,EAAM,IAAI,CAAI,GAAA,iBAAA,CAAkB,UAAW,CAAA,SAAA,CAAA;AAExD,MAAA,IAAI,CAAC,OAAS,EAAA;AACZ,QAAA,OAAA;AAAA,OACF;AAEA,MAAI,IAAA,CAAC,WAAe,IAAA,WAAA,KAAgB,IAAM,EAAA;AACxC,QAAA,OAAA,CAAQ,MAAM,SAAY,GAAA,sBAAA,CAAA;AAC1B,QAAA,OAAA,CAAQ,MAAM,OAAU,GAAA,MAAA,CAAA;AACxB,QAAA,OAAA;AAAA,OACF;AAEA,MAAQ,OAAA,CAAA,KAAA,CAAM,SAAY,GAAA,CAAA,YAAA,EAAe,WAAY,CAAA,CAAA,GAAI,IAAO,GAAA,IAAI,CAAO,IAAA,EAAA,WAAA,CAAY,CAAI,GAAA,IAAA,GAAO,IAAI,CAAA,MAAA,CAAA,CAAA;AACtG,MAAA,OAAA,CAAQ,MAAM,OAAU,GAAA,EAAA,CAAA;AAAA,KAC1B;AAEA,IAAO,MAAA,EAAA,CAAA;AAEP,IAAM,MAAA,iBAAA,GAAoB,MAAO,CAAA,SAAA,CAAU,MAAM,CAAA,CAAA;AACjD,IAAA,MAAM,uBAAuB,iBAAkB,CAAA,SAAA;AAAA,MAC7C,CAAC,OAAO,aAAkB,KAAA;AAExB,QAAI,IAAA,KAAA,CAAM,SAAc,KAAA,aAAA,CAAc,SAAW,EAAA;AAC/C,UAAO,MAAA,EAAA,CAAA;AAAA,SACT;AAAA,OACF;AAAA,KACF,CAAA;AACA,IAAM,MAAA,gBAAA,GAAmB,KAAK,MAAO,CAAA,MAAA,CAAO,UAAU,CAAC,EAAE,QAAa,KAAA;AACpE,MAAA,MAAM,QAAQ,MAAO,CAAA,IAAA,CAAK,CAACA,MAAUA,KAAAA,MAAAA,CAAM,iBAAiB,YAAY,CAAA,CAAA;AACxE,MAAA,MAAM,MAAS,GAAA,YAAA,CAAa,KAAO,EAAA,QAAA,CAAS,WAAW,CAAC,CAAA,CAAA;AAExD,MAAO,MAAA,CAAA,GAAA,CAAI,UAAU,IAAI,CAAA,CAAA;AAAA,KAC1B,CAAA,CAAA;AAED,IAAA,OAAO,MAAM;AACX,MAAA,MAAA,CAAO,OAAQ,EAAA,CAAA;AACf,MAAkB,iBAAA,EAAA,CAAA;AAClB,MAAqB,oBAAA,EAAA,CAAA;AACrB,MAAiB,gBAAA,EAAA,CAAA;AAAA,KACnB,CAAA;AAAA,KACC,CAAC,IAAA,EAAM,cAAc,WAAa,EAAA,WAAA,EAAa,iBAAiB,CAAC,CAAA,CAAA;AAEpE,EACE,uBAAA,GAAA;AAAA,IAAC,MAAA;AAAA,IAAA;AAAA,MACC,KAAA;AAAA,MACA,KAAO,EAAA,IAAA;AAAA,MACP,GAAK,EAAA,SAAA;AAAA,MACL,KAAA,EAAO,EAAE,OAAA,EAAS,MAAO,EAAA;AAAA,KAAA;AAAA,GAC3B,CAAA;AAEJ,CAAA;AASO,MAAM,OAAU,GAAA,UAAA;AAAA,EACrB,CACE,EAAE,SAAW,EAAA,QAAA,EAAU,cAAc,oBAAsB,EAAA,GAAG,KAAM,EAAA,EACpE,YACG,KAAA;AACH,IAAA,MAAM,YAAY,YAAa,EAAA,CAAA;AAC/B,IAAA,MAAM,mBAAmB,mBAAoB,EAAA,CAAA;AAC7C,IAAA,MAAM,sBAAsB,sBAAuB,EAAA,CAAA;AACnD,IAAA,MAAM,gBAAmB,GAAA,QAAA,CAAS,CAAC,KAAA,KAAU,MAAM,OAAO,CAAA,CAAA;AAC1D,IAAA,MAAM,oBAAoB,WAAY,EAAA,CAAA;AAEtC,IAAA,MAAM,iBAAoB,GAAA,WAAA;AAAA,MACxB,CAAC,KAAwB,KAAA;AACvB,QAAM,MAAA,SAAA,GAAY,iBAAkB,CAAA,QAAA,EAAW,CAAA,YAAA,CAAA;AAE/C,QAAA,IAAI,SAAW,EAAA;AACb,UAAA,OAAA;AAAA,SACF;AAEA,QAAiB,gBAAA,CAAA;AAAA,UACf,CAAC,WAAW,GAAG,SAAA,CAAU,oBAAqB,CAAA;AAAA,YAC5C,GAAG,KAAM,CAAA,OAAA;AAAA,YACT,GAAG,KAAM,CAAA,OAAA;AAAA,WACV,CAAA;AAAA,SACF,CAAA,CAAA;AAAA,OACH;AAAA,MACA,CAAC,gBAAA,EAAkB,WAAa,EAAA,SAAA,EAAW,iBAAiB,CAAA;AAAA,KAC9D,CAAA;AAEA,IAAM,MAAA,kBAAA,GAAqB,YAAY,MAAM;AAC3C,MAAA,gBAAA,CAAiB,EAAE,CAAC,WAAW,GAAG,MAAM,CAAA,CAAA;AAAA,KACvC,EAAA,CAAC,gBAAkB,EAAA,WAAW,CAAC,CAAA,CAAA;AAElC,IAAA,SAAA,CAAU,MAAM;AACd,MAAA,IAAI,CAAC,gBAAkB,EAAA;AACrB,QAAA,OAAA;AAAA,OACF;AAEA,MAAiB,gBAAA,CAAA,gBAAA,CAAiB,eAAe,iBAAiB,CAAA,CAAA;AAClE,MAAiB,gBAAA,CAAA,gBAAA,CAAiB,gBAAgB,kBAAkB,CAAA,CAAA;AACpE,MAAO,MAAA,CAAA,gBAAA,CAAiB,QAAQ,kBAAkB,CAAA,CAAA;AAElD,MAAA,OAAO,MAAM;AACX,QAAiB,gBAAA,CAAA,mBAAA,CAAoB,eAAe,iBAAiB,CAAA,CAAA;AACrE,QAAiB,gBAAA,CAAA,mBAAA;AAAA,UACf,cAAA;AAAA,UACA,kBAAA;AAAA,SACF,CAAA;AACA,QAAO,MAAA,CAAA,mBAAA,CAAoB,QAAQ,kBAAkB,CAAA,CAAA;AACrD,QAAmB,kBAAA,EAAA,CAAA;AAAA,OACrB,CAAA;AAAA,KACC,EAAA,CAAC,gBAAkB,EAAA,iBAAA,EAAmB,kBAAkB,CAAC,CAAA,CAAA;AAE5D,IACE,uBAAA,IAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,aAAW,EAAA,IAAA;AAAA,QACX,SAAA,EAAW,EAAG,CAAA,+BAAA,EAAiC,SAAS,CAAA;AAAA,QACvD,GAAG,KAAA;AAAA,QACJ,GAAK,EAAA,YAAA;AAAA,QAEJ,QAAA,EAAA;AAAA,UAAoB,mBAAA,CAAA,GAAA,CAAI,CAAC,YACxB,qBAAA,GAAA;AAAA,YAAC,cAAA;AAAA,YAAA;AAAA,cAEC,YAAA;AAAA,cACA,WAAA;AAAA,aAAA;AAAA,YAFK,YAAA;AAAA,WAIR,CAAA;AAAA,UAEA,QAAA;AAAA,SAAA;AAAA,OAAA;AAAA,KACH,CAAA;AAAA,GAEJ;AACF;;;;"}
|
package/dist/flow.cjs
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@liveblocks/core');
|
|
4
|
+
var react = require('@liveblocks/react');
|
|
5
|
+
var _private = require('@liveblocks/react/_private');
|
|
6
|
+
var react$2 = require('@xyflow/react');
|
|
7
|
+
var react$1 = require('react');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_STORAGE_KEY = "flow";
|
|
10
|
+
const NODE_LOCAL_KEYS = [
|
|
11
|
+
"selected",
|
|
12
|
+
"dragging",
|
|
13
|
+
"measured",
|
|
14
|
+
"resizing"
|
|
15
|
+
];
|
|
16
|
+
const EDGE_LOCAL_KEYS = ["selected"];
|
|
17
|
+
const EMPTY_ARRAY = [];
|
|
18
|
+
function pick(from, keys) {
|
|
19
|
+
const result = {};
|
|
20
|
+
for (const key of keys) {
|
|
21
|
+
const value = from[key];
|
|
22
|
+
if (value !== void 0 && value !== null) {
|
|
23
|
+
result[key] = value;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
function omit(from, keys) {
|
|
29
|
+
const result = { ...from };
|
|
30
|
+
for (const key of keys) {
|
|
31
|
+
delete result[key];
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
function reconcile(cache, next) {
|
|
36
|
+
const previous = cache.get(next.id);
|
|
37
|
+
if (previous && core.shallow(previous, next)) {
|
|
38
|
+
return previous;
|
|
39
|
+
}
|
|
40
|
+
cache.set(next.id, next);
|
|
41
|
+
return next;
|
|
42
|
+
}
|
|
43
|
+
function merge(cache, remote, local) {
|
|
44
|
+
for (const id of cache.keys()) {
|
|
45
|
+
if (!remote.has(id)) {
|
|
46
|
+
cache.delete(id);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const id of local.keys()) {
|
|
50
|
+
if (!remote.has(id)) {
|
|
51
|
+
local.delete(id);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return Array.from(remote.values(), (item) => {
|
|
55
|
+
const localItem = local.get(item.id);
|
|
56
|
+
return reconcile(
|
|
57
|
+
cache,
|
|
58
|
+
localItem ? Object.assign({}, item, localItem) : item
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function updateLocalState(map, key, changes) {
|
|
63
|
+
const next = {};
|
|
64
|
+
for (const change in changes) {
|
|
65
|
+
const value = changes[change];
|
|
66
|
+
if (value !== void 0 && value !== false) {
|
|
67
|
+
next[change] = value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (Object.keys(next).length > 0) {
|
|
71
|
+
map.set(key, next);
|
|
72
|
+
return true;
|
|
73
|
+
} else {
|
|
74
|
+
const hasItem = map.has(key);
|
|
75
|
+
map.delete(key);
|
|
76
|
+
return hasItem;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function nodeToStorage(node) {
|
|
80
|
+
const { data, ...rest } = omit(node, NODE_LOCAL_KEYS);
|
|
81
|
+
return new core.LiveObject({
|
|
82
|
+
...rest,
|
|
83
|
+
data: new core.LiveObject(data)
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function edgeToStorage(edge) {
|
|
87
|
+
const { data, ...rest } = omit(edge, EDGE_LOCAL_KEYS);
|
|
88
|
+
return new core.LiveObject({
|
|
89
|
+
...rest,
|
|
90
|
+
// `data` is optional on edges.
|
|
91
|
+
data: data === void 0 ? void 0 : new core.LiveObject(data)
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function applyNodeChanges(args) {
|
|
95
|
+
const { changes, nodes, nextLocal, nodeCache } = args;
|
|
96
|
+
let hasLocalChanged = false;
|
|
97
|
+
for (const change of changes) {
|
|
98
|
+
switch (change.type) {
|
|
99
|
+
case "add":
|
|
100
|
+
case "replace":
|
|
101
|
+
nodes.set(change.item.id, nodeToStorage(change.item));
|
|
102
|
+
if (updateLocalState(
|
|
103
|
+
nextLocal,
|
|
104
|
+
change.item.id,
|
|
105
|
+
pick(change.item, NODE_LOCAL_KEYS)
|
|
106
|
+
)) {
|
|
107
|
+
hasLocalChanged = true;
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
case "remove":
|
|
111
|
+
nodes.delete(change.id);
|
|
112
|
+
nodeCache.delete(change.id);
|
|
113
|
+
nextLocal.delete(change.id);
|
|
114
|
+
hasLocalChanged = true;
|
|
115
|
+
break;
|
|
116
|
+
case "position": {
|
|
117
|
+
const node = nodes.get(change.id);
|
|
118
|
+
if (!node || !change.position) {
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
const previous = node.get("position");
|
|
122
|
+
if (previous?.x !== change.position.x || previous?.y !== change.position.y) {
|
|
123
|
+
node.set("position", change.position);
|
|
124
|
+
}
|
|
125
|
+
if (change.dragging !== void 0) {
|
|
126
|
+
updateLocalState(nextLocal, change.id, {
|
|
127
|
+
...nextLocal.get(change.id),
|
|
128
|
+
dragging: change.dragging
|
|
129
|
+
});
|
|
130
|
+
hasLocalChanged = true;
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case "dimensions": {
|
|
135
|
+
const node = nodes.get(change.id);
|
|
136
|
+
const patch = {
|
|
137
|
+
...nextLocal.get(change.id)
|
|
138
|
+
};
|
|
139
|
+
if (node && change.dimensions !== void 0 && change.setAttributes !== void 0) {
|
|
140
|
+
if (change.setAttributes === true || change.setAttributes === "width") {
|
|
141
|
+
node.set("width", change.dimensions.width);
|
|
142
|
+
}
|
|
143
|
+
if (change.setAttributes === true || change.setAttributes === "height") {
|
|
144
|
+
node.set("height", change.dimensions.height);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (change.dimensions !== void 0) {
|
|
148
|
+
patch.measured = change.dimensions;
|
|
149
|
+
}
|
|
150
|
+
if (change.resizing !== void 0) {
|
|
151
|
+
patch.resizing = change.resizing;
|
|
152
|
+
}
|
|
153
|
+
updateLocalState(nextLocal, change.id, patch);
|
|
154
|
+
hasLocalChanged = true;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case "select":
|
|
158
|
+
updateLocalState(nextLocal, change.id, {
|
|
159
|
+
...nextLocal.get(change.id),
|
|
160
|
+
selected: change.selected
|
|
161
|
+
});
|
|
162
|
+
hasLocalChanged = true;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return hasLocalChanged;
|
|
167
|
+
}
|
|
168
|
+
function applyEdgeChanges(args) {
|
|
169
|
+
const { changes, edges, nextLocal, edgeCache } = args;
|
|
170
|
+
let hasLocalChanged = false;
|
|
171
|
+
for (const change of changes) {
|
|
172
|
+
switch (change.type) {
|
|
173
|
+
case "add":
|
|
174
|
+
case "replace":
|
|
175
|
+
edges.set(change.item.id, edgeToStorage(change.item));
|
|
176
|
+
if (updateLocalState(
|
|
177
|
+
nextLocal,
|
|
178
|
+
change.item.id,
|
|
179
|
+
pick(change.item, EDGE_LOCAL_KEYS)
|
|
180
|
+
)) {
|
|
181
|
+
hasLocalChanged = true;
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
case "remove":
|
|
185
|
+
edges.delete(change.id);
|
|
186
|
+
edgeCache.delete(change.id);
|
|
187
|
+
nextLocal.delete(change.id);
|
|
188
|
+
hasLocalChanged = true;
|
|
189
|
+
break;
|
|
190
|
+
case "select":
|
|
191
|
+
updateLocalState(nextLocal, change.id, {
|
|
192
|
+
...nextLocal.get(change.id),
|
|
193
|
+
selected: change.selected
|
|
194
|
+
});
|
|
195
|
+
hasLocalChanged = true;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return hasLocalChanged;
|
|
200
|
+
}
|
|
201
|
+
function createLiveblocksFlow(nodes = [], edges = []) {
|
|
202
|
+
return new core.LiveObject({
|
|
203
|
+
nodes: new core.LiveMap(nodes.map((node) => [node.id, nodeToStorage(node)])),
|
|
204
|
+
edges: new core.LiveMap(edges.map((edge) => [edge.id, edgeToStorage(edge)]))
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function useLiveblocksFlow(options = {}) {
|
|
208
|
+
const isStorageLoaded = react.useStorage(() => true) ?? false;
|
|
209
|
+
const frozenOptions = _private.useInitial({
|
|
210
|
+
initial: options.initial,
|
|
211
|
+
storageKey: options.storageKey ?? DEFAULT_STORAGE_KEY
|
|
212
|
+
});
|
|
213
|
+
const nodeCache = react$1.useRef(/* @__PURE__ */ new Map());
|
|
214
|
+
const edgeCache = react$1.useRef(/* @__PURE__ */ new Map());
|
|
215
|
+
const [localNodes\u03A3] = react$1.useState(
|
|
216
|
+
() => new core.Signal(/* @__PURE__ */ new Map())
|
|
217
|
+
);
|
|
218
|
+
const [localEdges\u03A3] = react$1.useState(
|
|
219
|
+
() => new core.Signal(/* @__PURE__ */ new Map())
|
|
220
|
+
);
|
|
221
|
+
const localNodes = _private.useSignal(localNodes\u03A3);
|
|
222
|
+
const localEdges = _private.useSignal(localEdges\u03A3);
|
|
223
|
+
const remoteNodesMap = react.useStorage((storage) => {
|
|
224
|
+
const flow = storage[frozenOptions.storageKey];
|
|
225
|
+
return flow?.nodes ?? null;
|
|
226
|
+
});
|
|
227
|
+
const remoteEdgesMap = react.useStorage((storage) => {
|
|
228
|
+
const flow = storage[frozenOptions.storageKey];
|
|
229
|
+
return flow?.edges ?? null;
|
|
230
|
+
});
|
|
231
|
+
const nodes = react$1.useMemo(
|
|
232
|
+
() => remoteNodesMap ? merge(nodeCache.current, remoteNodesMap, localNodes) : null,
|
|
233
|
+
[remoteNodesMap, localNodes]
|
|
234
|
+
);
|
|
235
|
+
const edges = react$1.useMemo(
|
|
236
|
+
() => remoteEdgesMap ? merge(edgeCache.current, remoteEdgesMap, localEdges) : null,
|
|
237
|
+
[remoteEdgesMap, localEdges]
|
|
238
|
+
);
|
|
239
|
+
const onNodesChange = react.useMutation(
|
|
240
|
+
({ storage }, changes) => {
|
|
241
|
+
const flow = storage.get(frozenOptions.storageKey);
|
|
242
|
+
if (!flow) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const nextLocal = new Map(localNodes\u03A3.get());
|
|
246
|
+
const hasLocalChanged = applyNodeChanges({
|
|
247
|
+
changes,
|
|
248
|
+
nodes: flow.get("nodes"),
|
|
249
|
+
nextLocal,
|
|
250
|
+
nodeCache: nodeCache.current
|
|
251
|
+
});
|
|
252
|
+
if (hasLocalChanged) {
|
|
253
|
+
localNodes\u03A3.set(nextLocal);
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
[]
|
|
257
|
+
);
|
|
258
|
+
const onEdgesChange = react.useMutation(
|
|
259
|
+
({ storage }, changes) => {
|
|
260
|
+
const flow = storage.get(frozenOptions.storageKey);
|
|
261
|
+
if (!flow) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const nextLocal = new Map(localEdges\u03A3.get());
|
|
265
|
+
const hasLocalChanged = applyEdgeChanges({
|
|
266
|
+
changes,
|
|
267
|
+
edges: flow.get("edges"),
|
|
268
|
+
nextLocal,
|
|
269
|
+
edgeCache: edgeCache.current
|
|
270
|
+
});
|
|
271
|
+
if (hasLocalChanged) {
|
|
272
|
+
localEdges\u03A3.set(nextLocal);
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
[]
|
|
276
|
+
);
|
|
277
|
+
const onConnect = react.useMutation(({ storage }, connection) => {
|
|
278
|
+
const flow = storage.get(frozenOptions.storageKey);
|
|
279
|
+
if (!flow) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const edges2 = flow.get("edges");
|
|
283
|
+
for (const edge of edges2.values()) {
|
|
284
|
+
if (edge.get("source") === connection.source && edge.get("target") === connection.target && (edge.get("sourceHandle") ?? null) === (connection.sourceHandle ?? null) && (edge.get("targetHandle") ?? null) === (connection.targetHandle ?? null)) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const [newEdge] = react$2.addEdge(connection, []);
|
|
289
|
+
if (!newEdge) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
edges2.set(newEdge.id, edgeToStorage(newEdge));
|
|
293
|
+
}, []);
|
|
294
|
+
const setInitialStorage = react.useMutation(({ storage }) => {
|
|
295
|
+
if (storage.get(frozenOptions.storageKey) !== void 0) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const { nodes: initialNodes = [], edges: initialEdges = [] } = frozenOptions.initial ?? {};
|
|
299
|
+
storage.set(
|
|
300
|
+
frozenOptions.storageKey,
|
|
301
|
+
createLiveblocksFlow(initialNodes, initialEdges)
|
|
302
|
+
);
|
|
303
|
+
}, []);
|
|
304
|
+
react$1.useEffect(() => {
|
|
305
|
+
if (isStorageLoaded) {
|
|
306
|
+
setInitialStorage();
|
|
307
|
+
}
|
|
308
|
+
}, [isStorageLoaded, setInitialStorage]);
|
|
309
|
+
return {
|
|
310
|
+
nodes,
|
|
311
|
+
edges,
|
|
312
|
+
isLoading: !isStorageLoaded,
|
|
313
|
+
onNodesChange,
|
|
314
|
+
onEdgesChange,
|
|
315
|
+
onConnect
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function useLiveblocksFlowSuspense(options = {}) {
|
|
319
|
+
const result = useLiveblocksFlow(options);
|
|
320
|
+
_private.useSuspendUntilStorageReady();
|
|
321
|
+
return {
|
|
322
|
+
...result,
|
|
323
|
+
nodes: result.nodes ?? EMPTY_ARRAY,
|
|
324
|
+
edges: result.edges ?? EMPTY_ARRAY,
|
|
325
|
+
isLoading: false
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
exports.createLiveblocksFlow = createLiveblocksFlow;
|
|
330
|
+
exports.useLiveblocksFlow = useLiveblocksFlow;
|
|
331
|
+
exports.useLiveblocksFlowSuspense = useLiveblocksFlowSuspense;
|
|
332
|
+
//# sourceMappingURL=flow.cjs.map
|