-
Notifications
You must be signed in to change notification settings - Fork 892
Expand file tree
/
Copy pathgraph-provider.tsx
More file actions
210 lines (197 loc) · 7.31 KB
/
Copy pathgraph-provider.tsx
File metadata and controls
210 lines (197 loc) · 7.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import type { dia } from '@joint/core';
import React from 'react';
import { useImperativeApi } from '../../hooks/use-imperative-api';
import { useIsomorphicLayoutEffect } from '../../hooks/use-isomorphic-layout-effect';
import { GraphStoreContext } from '../../context';
import { GraphStore } from '../../store';
/**
* True when running without a DOM (server-side rendering). Constant for the
* lifetime of the environment, so it is safe to branch rendering on it.
*/
const IS_SERVER = typeof document === 'undefined';
import type { AutoSizeOrigin } from '../../store/graph-store';
import type { OnIncrementalCellsChange } from '../../store/graph-projection';
import type { ElementJSONInit, LinkJSONInit } from '../../types/cell.types';
import type { CellInput } from '../../utils/normalize-cell-input';
/** Cells array accepted by GraphProvider. */
type ProviderCells<Element extends ElementJSONInit, Link extends LinkJSONInit> = ReadonlyArray<
Element | Link
>;
/**
* Props common to every `GraphProvider` mode.
* @template ElementData - User data attached to each element record.
* @template LinkData - User data attached to each link record.
*/
export interface GraphProviderProps<
Element extends ElementJSONInit = ElementJSONInit,
Link extends LinkJSONInit = LinkJSONInit,
> {
/**
* Pre-existing JointJS graph instance to use. If omitted, GraphProvider
* creates a fresh `new dia.Graph(...)`.
* @see https://docs.jointjs.com/api/dia/Graph
*/
readonly graph?: dia.Graph;
/** React children rendered inside the provider — typically a `<Paper />`. */
readonly children?: React.ReactNode;
/**
* Cell namespace passed through to `new dia.Graph`. Defaults to JointJS
* built-in shapes plus the `@joint/react` ElementModel and LinkModel.
*/
readonly cellNamespace?: unknown;
/** Custom cell model used as the base class for all cells in the graph. */
readonly cellModel?: typeof dia.Cell;
/**
* Reference point that stays fixed when an auto-sized element's measured
* size changes (via `useMeasureNode`). Mirrors CSS `transform-origin` semantics.
* - `'top-left'` (default): element grows right/down.
* - `'center'`: element grows symmetrically — its geometric center stays put.
*
* Only affects measurement-driven writes. Manual `cell.resize()`, interactive
* resize tools, and direct `cell.set('size', ...)` calls are unaffected.
* @default 'top-left'
*/
readonly autoSizeOrigin?: AutoSizeOrigin;
/** Pre-built `GraphStore` instance. When provided, GraphProvider does not own its lifecycle. */
readonly store?: GraphStore<Element, Link>;
/**
* Initial cells for uncontrolled mode. Ignored if `cells` is provided. Should not
*/
readonly initialCells?: ReadonlyArray<CellInput<Element, Link>>;
readonly cells?: ProviderCells<Element, Link>;
/** Notification-only callback — React state is NOT pushed back into the graph. */
readonly onCellsChange?: (newCells: ProviderCells<Element, Link>) => void;
/**
* Notification fired with granular `added` / `changed` / `removed` sets
* after each commit. Independent of controlled/uncontrolled mode.
*/
readonly onIncrementalCellsChange?: OnIncrementalCellsChange<Element, Link>;
}
/**
* Provider props normalised to the unparameterised base shape.
*
* Internally GraphProvider stores the `GraphStore` with default generics
* (`ElementAttributes` / `LinkAttributes`). Each `useGraphStore<E, L>()` call
* re-binds the generics on read — the runtime instance is the same.
*/
type GraphProviderBaseInternalProps = GraphProviderProps<ElementJSONInit, LinkJSONInit> & {
ref?: React.Ref<dia.Graph | null>;
};
/**
* Internal base component for GraphProvider.
*
* Operates exclusively on the base record shape so the runtime instance can
* flow into the unparameterised `GraphStoreContext` without a variance cast.
* The exported `GraphProvider` re-types this base to the caller's `<Element,
* Link>` parameters.
* @param props - GraphProvider props including optional forwarded ref.
* @returns The rendered graph context provider or null while loading.
*/
function GraphBase(props: GraphProviderBaseInternalProps) {
const {
children,
store,
onIncrementalCellsChange,
onCellsChange,
ref: forwardedRef,
graph,
cellNamespace,
cellModel,
autoSizeOrigin,
initialCells,
cells,
} = props;
const isControlled = !!cells;
const buildStore = (): GraphStore<ElementJSONInit, LinkJSONInit> =>
store ??
new GraphStore<ElementJSONInit, LinkJSONInit>({
graph,
cellNamespace,
cellModel,
initialCells: cells ?? initialCells ?? [],
autoSizeOrigin,
});
// Client: the store is owned by a layout-effect lifecycle (StrictMode-safe
// create/cleanup). `isReady` stays false on the server because layout effects
// never run there — that is handled by the SSR branch below.
const { isReady, ref } = useImperativeApi<GraphStore<ElementJSONInit, LinkJSONInit>, dia.Graph>(
{
instanceSelector: (instance) => instance.graph,
forwardedRef,
onLoad() {
const graphStore = buildStore();
return {
cleanup() {
if (store) return;
graphStore.destroy(!!graph);
},
instance: graphStore,
};
},
},
[]
);
useIsomorphicLayoutEffect(() => {
if (!isReady) return;
ref.current.setOnIncrementalCellsChange((changeSet) => {
onIncrementalCellsChange?.(changeSet);
if (onCellsChange) {
onCellsChange([...ref.current.graphProjection.cells.getAll()]);
return;
}
if (isControlled) {
ref.current.applyControlled(cells);
}
});
if (isControlled) {
ref.current.applyControlled(cells ?? []);
}
}, [isReady, onIncrementalCellsChange, onCellsChange, ref, isControlled, cells]);
if (!isReady) {
// Server render: provide a synchronous, per-request store so children and
// data hooks (`useCells`, ...) render to HTML. `GraphStore` is pure data
// (no DOM), so this is SSR-safe; `<Paper>` degrades to its host element.
// On the client this branch is never reached once the layout effect runs.
if (IS_SERVER) {
return <GraphStoreContext.Provider value={buildStore()}>{children}</GraphStoreContext.Provider>;
}
return null;
}
return <GraphStoreContext.Provider value={ref.current}>{children}</GraphStoreContext.Provider>;
}
/**
* GraphProvider supplies graph context to its children.
*
* **Modes of operation:**
*
* 1. **Uncontrolled** (JointJS owns the graph after mount):
* ```tsx
* <GraphProvider initialCells={[...]}>
* <Paper />
* </GraphProvider>
* ```
*
* 2. **Controlled** (React owns the cells array):
* ```tsx
* const [cells, setCells] = useState<readonly CellRecord[]>([...]);
* <GraphProvider cells={cells} onCellsChange={setCells}>
* <Paper />
* </GraphProvider>
* ```
*
* 3. **Incremental-notification** (external store, Redux/Zustand):
* ```tsx
* <GraphProvider onIncrementalCellsChange={(c) => dispatch(c)}>
* <Paper />
* </GraphProvider>
* ```
* @see GraphProviderProps for all available props
*/
export const GraphProvider = GraphBase as <
Element extends ElementJSONInit = ElementJSONInit,
Link extends LinkJSONInit = LinkJSONInit,
>(
props: GraphProviderProps<Element, Link> & {
ref?: React.Ref<dia.Graph | null>;
}
) => ReturnType<typeof GraphBase>;