@blockslides/react 0.1.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.
@@ -0,0 +1,69 @@
1
+ import type { FloatingMenuPluginProps } from '@blockslides/extension-floating-menu'
2
+ import { FloatingMenuPlugin } from '@blockslides/extension-floating-menu'
3
+ import { useCurrentEditor } from '@blockslides/react'
4
+ import React, { useEffect, useRef } from 'react'
5
+ import { createPortal } from 'react-dom'
6
+
7
+ type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
8
+
9
+ export type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKey'>, 'element' | 'editor'> & {
10
+ editor: FloatingMenuPluginProps['editor'] | null
11
+ options?: FloatingMenuPluginProps['options']
12
+ } & React.HTMLAttributes<HTMLDivElement>
13
+
14
+ export const FloatingMenu = React.forwardRef<HTMLDivElement, FloatingMenuProps>(
15
+ ({ pluginKey = 'floatingMenu', editor, appendTo, shouldShow = null, options, children, ...restProps }, ref) => {
16
+ const menuEl = useRef(document.createElement('div'))
17
+
18
+ if (typeof ref === 'function') {
19
+ ref(menuEl.current)
20
+ } else if (ref) {
21
+ ref.current = menuEl.current
22
+ }
23
+
24
+ const { editor: currentEditor } = useCurrentEditor()
25
+
26
+ useEffect(() => {
27
+ const floatingMenuElement = menuEl.current
28
+
29
+ floatingMenuElement.style.visibility = 'hidden'
30
+ floatingMenuElement.style.position = 'absolute'
31
+
32
+ if (editor?.isDestroyed || (currentEditor as any)?.isDestroyed) {
33
+ return
34
+ }
35
+
36
+ const attachToEditor = editor || currentEditor
37
+
38
+ if (!attachToEditor) {
39
+ console.warn(
40
+ 'FloatingMenu component is not rendered inside of an editor component or does not have editor prop.',
41
+ )
42
+ return
43
+ }
44
+
45
+ const plugin = FloatingMenuPlugin({
46
+ editor: attachToEditor,
47
+ element: floatingMenuElement,
48
+ pluginKey,
49
+ appendTo,
50
+ shouldShow,
51
+ options,
52
+ })
53
+
54
+ attachToEditor.registerPlugin(plugin)
55
+
56
+ return () => {
57
+ attachToEditor.unregisterPlugin(pluginKey)
58
+ window.requestAnimationFrame(() => {
59
+ if (floatingMenuElement.parentNode) {
60
+ floatingMenuElement.parentNode.removeChild(floatingMenuElement)
61
+ }
62
+ })
63
+ }
64
+ // eslint-disable-next-line react-hooks/exhaustive-deps
65
+ }, [editor, currentEditor, appendTo, pluginKey, shouldShow, options])
66
+
67
+ return createPortal(<div {...restProps}>{children}</div>, menuEl.current)
68
+ },
69
+ )
@@ -0,0 +1,2 @@
1
+ export * from './BubbleMenu.js'
2
+ export * from './FloatingMenu.js'
package/src/types.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { NodeViewProps as CoreNodeViewProps } from "@blockslides/core";
2
+ import type React from "react";
3
+
4
+ export type ReactNodeViewProps<T = HTMLElement> = CoreNodeViewProps & {
5
+ ref: React.RefObject<T | null>;
6
+ };
@@ -0,0 +1,405 @@
1
+ import { type EditorOptions, Editor } from "@blockslides/core";
2
+ import type { DependencyList, MutableRefObject } from "react";
3
+ import { useDebugValue, useEffect, useRef, useState } from "react";
4
+ import { useSyncExternalStore } from "use-sync-external-store/shim/index.js";
5
+
6
+ import { useEditorState } from "./useEditorState.js";
7
+
8
+ // @ts-ignore
9
+ const isDev = process.env.NODE_ENV !== "production";
10
+ const isSSR = typeof window === "undefined";
11
+ const isNext =
12
+ isSSR || Boolean(typeof window !== "undefined" && (window as any).next);
13
+
14
+ /**
15
+ * The options for the `useEditor` hook.
16
+ */
17
+ export type UseEditorOptions = Partial<EditorOptions> & {
18
+ /**
19
+ * Whether to render the editor on the first render.
20
+ * If client-side rendering, set this to `true`.
21
+ * If server-side rendering, set this to `false`.
22
+ * @default true
23
+ */
24
+ immediatelyRender?: boolean;
25
+ /**
26
+ * Whether to re-render the editor on each transaction.
27
+ * This is legacy behavior that will be removed in future versions.
28
+ * @default false
29
+ */
30
+ shouldRerenderOnTransaction?: boolean;
31
+ };
32
+
33
+ /**
34
+ * This class handles the creation, destruction, and re-creation of the editor instance.
35
+ */
36
+ class EditorInstanceManager {
37
+ /**
38
+ * The current editor instance.
39
+ */
40
+ private editor: Editor | null = null;
41
+
42
+ /**
43
+ * The most recent options to apply to the editor.
44
+ */
45
+ private options: MutableRefObject<UseEditorOptions>;
46
+
47
+ /**
48
+ * The subscriptions to notify when the editor instance
49
+ * has been created or destroyed.
50
+ */
51
+ private subscriptions = new Set<() => void>();
52
+
53
+ /**
54
+ * A timeout to destroy the editor if it was not mounted within a time frame.
55
+ */
56
+ private scheduledDestructionTimeout:
57
+ | ReturnType<typeof setTimeout>
58
+ | undefined;
59
+
60
+ /**
61
+ * Whether the editor has been mounted.
62
+ */
63
+ private isComponentMounted = false;
64
+
65
+ /**
66
+ * The most recent dependencies array.
67
+ */
68
+ private previousDeps: DependencyList | null = null;
69
+
70
+ /**
71
+ * The unique instance ID. This is used to identify the editor instance. And will be re-generated for each new instance.
72
+ */
73
+ public instanceId = "";
74
+
75
+ constructor(options: MutableRefObject<UseEditorOptions>) {
76
+ this.options = options;
77
+ this.subscriptions = new Set<() => void>();
78
+ this.setEditor(this.getInitialEditor());
79
+ this.scheduleDestroy();
80
+
81
+ this.getEditor = this.getEditor.bind(this);
82
+ this.getServerSnapshot = this.getServerSnapshot.bind(this);
83
+ this.subscribe = this.subscribe.bind(this);
84
+ this.refreshEditorInstance = this.refreshEditorInstance.bind(this);
85
+ this.scheduleDestroy = this.scheduleDestroy.bind(this);
86
+ this.onRender = this.onRender.bind(this);
87
+ this.createEditor = this.createEditor.bind(this);
88
+ }
89
+
90
+ private setEditor(editor: Editor | null) {
91
+ this.editor = editor;
92
+ this.instanceId = Math.random().toString(36).slice(2, 9);
93
+
94
+ // Notify all subscribers that the editor instance has been created
95
+ this.subscriptions.forEach((cb) => cb());
96
+ }
97
+
98
+ private getInitialEditor() {
99
+ if (this.options.current.immediatelyRender === undefined) {
100
+ if (isSSR || isNext) {
101
+ if (isDev) {
102
+ /**
103
+ * Throw an error in development, to make sure the developer is aware that the editor cannot be SSR'd
104
+ * and that they need to set `immediatelyRender` to `false` to avoid hydration mismatches.
105
+ */
106
+ throw new Error(
107
+ "Editor Error: SSR has been detected, please set `immediatelyRender` explicitly to `false` to avoid hydration mismatches."
108
+ );
109
+ }
110
+
111
+ // Best faith effort in production, run the code in the legacy mode to avoid hydration mismatches and errors in production
112
+ return null;
113
+ }
114
+
115
+ // Default to immediately rendering when client-side rendering
116
+ return this.createEditor();
117
+ }
118
+
119
+ if (this.options.current.immediatelyRender && isSSR && isDev) {
120
+ // Warn in development, to make sure the developer is aware that the editor cannot be SSR'd, set `immediatelyRender` to `false` to avoid hydration mismatches.
121
+ throw new Error(
122
+ "Editor Error: SSR has been detected, and `immediatelyRender` has been set to `true` this is an unsupported configuration that may result in errors, explicitly set `immediatelyRender` to `false` to avoid hydration mismatches."
123
+ );
124
+ }
125
+
126
+ if (this.options.current.immediatelyRender) {
127
+ return this.createEditor();
128
+ }
129
+
130
+ return null;
131
+ }
132
+
133
+ /**
134
+ * Create a new editor instance. And attach event listeners.
135
+ */
136
+ private createEditor(): Editor {
137
+ const optionsToApply: Partial<EditorOptions> = {
138
+ ...this.options.current,
139
+ // Always call the most recent version of the callback function by default
140
+ onBeforeCreate: (...args) =>
141
+ this.options.current.onBeforeCreate?.(...args),
142
+ onBlur: (...args) => this.options.current.onBlur?.(...args),
143
+ onCreate: (...args) => this.options.current.onCreate?.(...args),
144
+ onDestroy: (...args) => this.options.current.onDestroy?.(...args),
145
+ onFocus: (...args) => this.options.current.onFocus?.(...args),
146
+ onSelectionUpdate: (...args) =>
147
+ this.options.current.onSelectionUpdate?.(...args),
148
+ onTransaction: (...args) => this.options.current.onTransaction?.(...args),
149
+ onUpdate: (...args) => this.options.current.onUpdate?.(...args),
150
+ onContentError: (...args) =>
151
+ this.options.current.onContentError?.(...args),
152
+ onDrop: (...args) => this.options.current.onDrop?.(...args),
153
+ onPaste: (...args) => this.options.current.onPaste?.(...args),
154
+ onDelete: (...args) => this.options.current.onDelete?.(...args),
155
+ };
156
+ const editor = new Editor(optionsToApply);
157
+
158
+ // no need to keep track of the event listeners, they will be removed when the editor is destroyed
159
+
160
+ return editor;
161
+ }
162
+
163
+ /**
164
+ * Get the current editor instance.
165
+ */
166
+ getEditor(): Editor | null {
167
+ return this.editor;
168
+ }
169
+
170
+ /**
171
+ * Always disable the editor on the server-side.
172
+ */
173
+ getServerSnapshot(): null {
174
+ return null;
175
+ }
176
+
177
+ /**
178
+ * Subscribe to the editor instance's changes.
179
+ */
180
+ subscribe(onStoreChange: () => void) {
181
+ this.subscriptions.add(onStoreChange);
182
+
183
+ return () => {
184
+ this.subscriptions.delete(onStoreChange);
185
+ };
186
+ }
187
+
188
+ static compareOptions(a: UseEditorOptions, b: UseEditorOptions) {
189
+ return (Object.keys(a) as (keyof UseEditorOptions)[]).every((key) => {
190
+ if (
191
+ [
192
+ "onCreate",
193
+ "onBeforeCreate",
194
+ "onDestroy",
195
+ "onUpdate",
196
+ "onTransaction",
197
+ "onFocus",
198
+ "onBlur",
199
+ "onSelectionUpdate",
200
+ "onContentError",
201
+ "onDrop",
202
+ "onPaste",
203
+ ].includes(key)
204
+ ) {
205
+ // we don't want to compare callbacks, they are always different and only registered once
206
+ return true;
207
+ }
208
+
209
+ // We often encourage putting extensions inlined in the options object, so we will do a slightly deeper comparison here
210
+ if (key === "extensions" && a.extensions && b.extensions) {
211
+ if (a.extensions.length !== b.extensions.length) {
212
+ return false;
213
+ }
214
+ return a.extensions.every((extension, index) => {
215
+ if (extension !== b.extensions?.[index]) {
216
+ return false;
217
+ }
218
+ return true;
219
+ });
220
+ }
221
+ if (a[key] !== b[key]) {
222
+ // if any of the options have changed, we should update the editor options
223
+ return false;
224
+ }
225
+ return true;
226
+ });
227
+ }
228
+
229
+ /**
230
+ * On each render, we will create, update, or destroy the editor instance.
231
+ * @param deps The dependencies to watch for changes
232
+ * @returns A cleanup function
233
+ */
234
+ onRender(deps: DependencyList) {
235
+ // The returned callback will run on each render
236
+ return () => {
237
+ this.isComponentMounted = true;
238
+ // Cleanup any scheduled destructions, since we are currently rendering
239
+ clearTimeout(this.scheduledDestructionTimeout);
240
+
241
+ if (this.editor && !this.editor.isDestroyed && deps.length === 0) {
242
+ // if the editor does exist & deps are empty, we don't need to re-initialize the editor generally
243
+ if (
244
+ !EditorInstanceManager.compareOptions(
245
+ this.options.current,
246
+ this.editor.options
247
+ )
248
+ ) {
249
+ // But, the options are different, so we need to update the editor options
250
+ // Still, this is faster than re-creating the editor
251
+ this.editor.setOptions({
252
+ ...this.options.current,
253
+ editable: this.editor.isEditable,
254
+ });
255
+ }
256
+ } else {
257
+ // When the editor:
258
+ // - does not yet exist
259
+ // - is destroyed
260
+ // - the deps array changes
261
+ // We need to destroy the editor instance and re-initialize it
262
+ this.refreshEditorInstance(deps);
263
+ }
264
+
265
+ return () => {
266
+ this.isComponentMounted = false;
267
+ this.scheduleDestroy();
268
+ };
269
+ };
270
+ }
271
+
272
+ /**
273
+ * Recreate the editor instance if the dependencies have changed.
274
+ */
275
+ private refreshEditorInstance(deps: DependencyList) {
276
+ if (this.editor && !this.editor.isDestroyed) {
277
+ // Editor instance already exists
278
+ if (this.previousDeps === null) {
279
+ // If lastDeps has not yet been initialized, reuse the current editor instance
280
+ this.previousDeps = deps;
281
+ return;
282
+ }
283
+ const depsAreEqual =
284
+ this.previousDeps.length === deps.length &&
285
+ this.previousDeps.every((dep, index) => dep === deps[index]);
286
+
287
+ if (depsAreEqual) {
288
+ // deps exist and are equal, no need to recreate
289
+ return;
290
+ }
291
+ }
292
+
293
+ if (this.editor && !this.editor.isDestroyed) {
294
+ // Destroy the editor instance if it exists
295
+ this.editor.destroy();
296
+ }
297
+
298
+ this.setEditor(this.createEditor());
299
+
300
+ // Update the lastDeps to the current deps
301
+ this.previousDeps = deps;
302
+ }
303
+
304
+ /**
305
+ * Schedule the destruction of the editor instance.
306
+ * This will only destroy the editor if it was not mounted on the next tick.
307
+ * This is to avoid destroying the editor instance when it's actually still mounted.
308
+ */
309
+ private scheduleDestroy() {
310
+ const currentInstanceId = this.instanceId;
311
+ const currentEditor = this.editor;
312
+
313
+ // Wait two ticks to see if the component is still mounted
314
+ this.scheduledDestructionTimeout = setTimeout(() => {
315
+ if (this.isComponentMounted && this.instanceId === currentInstanceId) {
316
+ // If still mounted on the following tick, with the same instanceId, do not destroy the editor
317
+ if (currentEditor) {
318
+ // just re-apply options as they might have changed
319
+ currentEditor.setOptions(this.options.current);
320
+ }
321
+ return;
322
+ }
323
+ if (currentEditor && !currentEditor.isDestroyed) {
324
+ currentEditor.destroy();
325
+ if (this.instanceId === currentInstanceId) {
326
+ this.setEditor(null);
327
+ }
328
+ }
329
+ // This allows the effect to run again between ticks
330
+ // which may save us from having to re-create the editor
331
+ }, 1);
332
+ }
333
+ }
334
+
335
+ /**
336
+ * This hook allows you to create an editor instance.
337
+ * @param options The editor options
338
+ * @param deps The dependencies to watch for changes
339
+ * @returns The editor instance
340
+ * @example const editor = useEditor({ extensions: [...] })
341
+ */
342
+ export function useEditor(
343
+ options: UseEditorOptions & { immediatelyRender: false },
344
+ deps?: DependencyList
345
+ ): Editor | null;
346
+
347
+ /**
348
+ * This hook allows you to create an editor instance.
349
+ * @param options The editor options
350
+ * @param deps The dependencies to watch for changes
351
+ * @returns The editor instance
352
+ * @example const editor = useEditor({ extensions: [...] })
353
+ */
354
+ export function useEditor(
355
+ options: UseEditorOptions,
356
+ deps?: DependencyList
357
+ ): Editor;
358
+
359
+ export function useEditor(
360
+ options: UseEditorOptions = {},
361
+ deps: DependencyList = []
362
+ ): Editor | null {
363
+ const mostRecentOptions = useRef(options);
364
+
365
+ mostRecentOptions.current = options;
366
+
367
+ const [instanceManager] = useState(
368
+ () => new EditorInstanceManager(mostRecentOptions)
369
+ );
370
+
371
+ const editor = useSyncExternalStore(
372
+ instanceManager.subscribe,
373
+ instanceManager.getEditor,
374
+ instanceManager.getServerSnapshot
375
+ );
376
+
377
+ useDebugValue(editor);
378
+
379
+ // This effect will handle creating/updating the editor instance
380
+ // eslint-disable-next-line react-hooks/exhaustive-deps
381
+ useEffect(instanceManager.onRender(deps));
382
+
383
+ // The default behavior is to re-render on each transaction
384
+ // This is legacy behavior that will be removed in future versions
385
+ useEditorState({
386
+ editor,
387
+ selector: ({ transactionNumber }) => {
388
+ if (
389
+ options.shouldRerenderOnTransaction === false ||
390
+ options.shouldRerenderOnTransaction === undefined
391
+ ) {
392
+ // This will prevent the editor from re-rendering on each transaction
393
+ return null;
394
+ }
395
+
396
+ // This will avoid re-rendering on the first transaction when `immediatelyRender` is set to `true`
397
+ if (options.immediatelyRender && transactionNumber === 0) {
398
+ return 0;
399
+ }
400
+ return transactionNumber + 1;
401
+ },
402
+ });
403
+
404
+ return editor;
405
+ }
@@ -0,0 +1,188 @@
1
+ import type { Editor } from "@blockslides/core";
2
+ import deepEqual from "fast-deep-equal/es6/react.js";
3
+ import { useDebugValue, useEffect, useLayoutEffect, useState } from "react";
4
+ import { useSyncExternalStoreWithSelector } from "use-sync-external-store/shim/with-selector.js";
5
+
6
+ const useIsomorphicLayoutEffect =
7
+ typeof window !== "undefined" ? useLayoutEffect : useEffect;
8
+
9
+ export type EditorStateSnapshot<TEditor extends Editor | null = Editor | null> =
10
+ {
11
+ editor: TEditor;
12
+ transactionNumber: number;
13
+ };
14
+
15
+ export type UseEditorStateOptions<
16
+ TSelectorResult,
17
+ TEditor extends Editor | null = Editor | null
18
+ > = {
19
+ /**
20
+ * The editor instance.
21
+ */
22
+ editor: TEditor;
23
+ /**
24
+ * A selector function to determine the value to compare for re-rendering.
25
+ */
26
+ selector: (context: EditorStateSnapshot<TEditor>) => TSelectorResult;
27
+ /**
28
+ * A custom equality function to determine if the editor should re-render.
29
+ * @default `deepEqual` from `fast-deep-equal`
30
+ */
31
+ equalityFn?: (a: TSelectorResult, b: TSelectorResult | null) => boolean;
32
+ };
33
+
34
+ /**
35
+ * To synchronize the editor instance with the component state,
36
+ * we need to create a separate instance that is not affected by the component re-renders.
37
+ */
38
+ class EditorStateManager<TEditor extends Editor | null = Editor | null> {
39
+ private transactionNumber = 0;
40
+
41
+ private lastTransactionNumber = 0;
42
+
43
+ private lastSnapshot: EditorStateSnapshot<TEditor>;
44
+
45
+ private editor: TEditor;
46
+
47
+ private subscribers = new Set<() => void>();
48
+
49
+ constructor(initialEditor: TEditor) {
50
+ this.editor = initialEditor;
51
+ this.lastSnapshot = { editor: initialEditor, transactionNumber: 0 };
52
+
53
+ this.getSnapshot = this.getSnapshot.bind(this);
54
+ this.getServerSnapshot = this.getServerSnapshot.bind(this);
55
+ this.watch = this.watch.bind(this);
56
+ this.subscribe = this.subscribe.bind(this);
57
+ }
58
+
59
+ /**
60
+ * Get the current editor instance.
61
+ */
62
+ getSnapshot(): EditorStateSnapshot<TEditor> {
63
+ if (this.transactionNumber === this.lastTransactionNumber) {
64
+ return this.lastSnapshot;
65
+ }
66
+ this.lastTransactionNumber = this.transactionNumber;
67
+ this.lastSnapshot = {
68
+ editor: this.editor,
69
+ transactionNumber: this.transactionNumber,
70
+ };
71
+ return this.lastSnapshot;
72
+ }
73
+
74
+ /**
75
+ * Always disable the editor on the server-side.
76
+ */
77
+ getServerSnapshot(): EditorStateSnapshot<null> {
78
+ return { editor: null, transactionNumber: 0 };
79
+ }
80
+
81
+ /**
82
+ * Subscribe to the editor instance's changes.
83
+ */
84
+ subscribe(callback: () => void): () => void {
85
+ this.subscribers.add(callback);
86
+ return () => {
87
+ this.subscribers.delete(callback);
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Watch the editor instance for changes.
93
+ */
94
+ watch(nextEditor: Editor | null): undefined | (() => void) {
95
+ this.editor = nextEditor as TEditor;
96
+
97
+ if (this.editor) {
98
+ /**
99
+ * This will force a re-render when the editor state changes.
100
+ * This is to support things like `editor.can().toggleBold()` in components that `useEditor`.
101
+ * This could be more efficient, but it's a good trade-off for now.
102
+ */
103
+ const fn = () => {
104
+ this.transactionNumber += 1;
105
+ this.subscribers.forEach((callback) => callback());
106
+ };
107
+
108
+ const currentEditor = this.editor;
109
+
110
+ currentEditor.on("transaction", fn);
111
+ return () => {
112
+ currentEditor.off("transaction", fn);
113
+ };
114
+ }
115
+
116
+ return undefined;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * This hook allows you to watch for changes on the editor instance.
122
+ * It will allow you to select a part of the editor state and re-render the component when it changes.
123
+ * @example
124
+ * ```tsx
125
+ * const editor = useEditor({...options})
126
+ * const { currentSelection } = useEditorState({
127
+ * editor,
128
+ * selector: snapshot => ({ currentSelection: snapshot.editor.state.selection }),
129
+ * })
130
+ */
131
+ export function useEditorState<TSelectorResult>(
132
+ options: UseEditorStateOptions<TSelectorResult, Editor>
133
+ ): TSelectorResult;
134
+ /**
135
+ * This hook allows you to watch for changes on the editor instance.
136
+ * It will allow you to select a part of the editor state and re-render the component when it changes.
137
+ * @example
138
+ * ```tsx
139
+ * const editor = useEditor({...options})
140
+ * const { currentSelection } = useEditorState({
141
+ * editor,
142
+ * selector: snapshot => ({ currentSelection: snapshot.editor.state.selection }),
143
+ * })
144
+ */
145
+ export function useEditorState<TSelectorResult>(
146
+ options: UseEditorStateOptions<TSelectorResult, Editor | null>
147
+ ): TSelectorResult | null;
148
+
149
+ /**
150
+ * This hook allows you to watch for changes on the editor instance.
151
+ * It will allow you to select a part of the editor state and re-render the component when it changes.
152
+ * @example
153
+ * ```tsx
154
+ * const editor = useEditor({...options})
155
+ * const { currentSelection } = useEditorState({
156
+ * editor,
157
+ * selector: snapshot => ({ currentSelection: snapshot.editor.state.selection }),
158
+ * })
159
+ */
160
+ export function useEditorState<TSelectorResult>(
161
+ options:
162
+ | UseEditorStateOptions<TSelectorResult, Editor>
163
+ | UseEditorStateOptions<TSelectorResult, Editor | null>
164
+ ): TSelectorResult | null {
165
+ const [editorStateManager] = useState(
166
+ () => new EditorStateManager(options.editor)
167
+ );
168
+
169
+ // Using the `useSyncExternalStore` hook to sync the editor instance with the component state
170
+ const selectedState = useSyncExternalStoreWithSelector(
171
+ editorStateManager.subscribe,
172
+ editorStateManager.getSnapshot,
173
+ editorStateManager.getServerSnapshot,
174
+ options.selector as UseEditorStateOptions<
175
+ TSelectorResult,
176
+ Editor | null
177
+ >["selector"],
178
+ options.equalityFn ?? deepEqual
179
+ );
180
+
181
+ useIsomorphicLayoutEffect(() => {
182
+ return editorStateManager.watch(options.editor);
183
+ }, [options.editor, editorStateManager]);
184
+
185
+ useDebugValue(selectedState);
186
+
187
+ return selectedState;
188
+ }
@@ -0,0 +1,28 @@
1
+ import type { ReactNode } from 'react'
2
+ import { createContext, createElement, useContext } from 'react'
3
+
4
+ export interface ReactNodeViewContextProps {
5
+ onDragStart?: (event: DragEvent) => void
6
+ nodeViewContentRef?: (element: HTMLElement | null) => void
7
+ /**
8
+ * This allows you to add children into the NodeViewContent component.
9
+ * This is useful when statically rendering the content of a node view.
10
+ */
11
+ nodeViewContentChildren?: ReactNode
12
+ }
13
+
14
+ export const ReactNodeViewContext = createContext<ReactNodeViewContextProps>({
15
+ onDragStart: () => {
16
+ // no-op
17
+ },
18
+ nodeViewContentChildren: undefined,
19
+ nodeViewContentRef: () => {
20
+ // no-op
21
+ },
22
+ })
23
+
24
+ export const ReactNodeViewContentProvider = ({ children, content }: { children: ReactNode; content: ReactNode }) => {
25
+ return createElement(ReactNodeViewContext.Provider, { value: { nodeViewContentChildren: content } }, children)
26
+ }
27
+
28
+ export const useReactNodeView = () => useContext(ReactNodeViewContext)