@domternal/react 0.7.5 → 0.9.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/index.d.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { DependencyList, ReactNode, HTMLAttributes, Ref, RefObject, KeyboardEvent, ElementType, RefCallback } from 'react';
3
- import { AnyExtension, Content, FocusPosition, Editor, JSONContent, IconSet, ToolbarLayoutEntry, BubbleMenuOptions, FloatingMenuItemsOverride } from '@domternal/core';
3
+ import { AnyExtension, Content, FocusPosition, Editor, JSONContent, IconSet, ToolbarLayoutEntry, BubbleMenuOptions, FloatingMenuOptions, FloatingMenuItemsOverride, FloatingMenuKeymap } from '@domternal/core';
4
4
  export { AnyExtension, Content, Editor, FocusPosition, GenerateHTMLOptions, GenerateJSONOptions, GenerateTextOptions, JSONContent, generateHTML, generateJSON, generateText } from '@domternal/core';
5
- import { FloatingMenuOptions, FloatingMenuKeymap } from '@domternal/extension-block-menu';
6
5
 
7
6
  declare const DEFAULT_EXTENSIONS: AnyExtension[];
8
7
  interface UseEditorOptions {
@@ -17,10 +16,12 @@ interface UseEditorOptions {
17
16
  /** Output format for content comparison. @default 'html' */
18
17
  outputFormat?: 'html' | 'json';
19
18
  /**
20
- * Set to false to delay editor creation to useEffect (SSR-safe).
21
- * When false, the editor will be null during server-side rendering
22
- * and created only after the component mounts in the browser.
23
- * @default true
19
+ * Create the editor synchronously during the first render so `editor`
20
+ * is available immediately, with no null flash on first paint. The view
21
+ * starts in a detached element and is adopted into the mount point by
22
+ * the mount effect. Client-only: the default defers creation to a mount
23
+ * effect, which never runs during server-side rendering.
24
+ * @default false
24
25
  */
25
26
  immediatelyRender?: boolean;
26
27
  /** Called when the editor instance is created. */
@@ -60,12 +61,12 @@ interface UseEditorOptions {
60
61
  * return <div className="dm-editor"><div ref={editorRef} /></div>;
61
62
  * ```
62
63
  *
63
- * @example SSR-safe usage (Next.js)
64
+ * @example Editor available on the very first render (client-only apps)
64
65
  * ```tsx
65
66
  * const { editor, editorRef } = useEditor({
66
67
  * extensions,
67
68
  * content,
68
- * immediatelyRender: false,
69
+ * immediatelyRender: true,
69
70
  * });
70
71
  * ```
71
72
  *
@@ -238,14 +239,22 @@ interface DomternalProps extends UseEditorOptions {
238
239
  * </Domternal>
239
240
  * ```
240
241
  *
241
- * @example SSR-safe with loading state
242
+ * @example Loading state (shown during SSR and until the editor exists)
242
243
  * ```tsx
243
- * <Domternal extensions={extensions} immediatelyRender={false}>
244
+ * <Domternal extensions={extensions}>
244
245
  * <Domternal.Loading>Loading editor...</Domternal.Loading>
245
246
  * <Domternal.Toolbar />
246
247
  * <Domternal.Content />
247
248
  * </Domternal>
248
249
  * ```
250
+ *
251
+ * @example Editor on the very first render (client-only apps)
252
+ * ```tsx
253
+ * <Domternal extensions={extensions} immediatelyRender>
254
+ * <Domternal.Toolbar />
255
+ * <Domternal.Content />
256
+ * </Domternal>
257
+ * ```
249
258
  */
250
259
  declare function Domternal({ children, deps, ...options }: DomternalProps): ReactNode;
251
260
  declare namespace Domternal {
@@ -448,8 +457,8 @@ interface ReactNodeViewProps {
448
457
  node: PMNode;
449
458
  /** Whether this node is selected via NodeSelection. */
450
459
  selected: boolean;
451
- /** Get the document position of this node. */
452
- getPos: () => number;
460
+ /** Get the document position of this node. Returns `undefined` once the node is no longer in the document. */
461
+ getPos: () => number | undefined;
453
462
  /** Update the node's attributes. */
454
463
  updateAttributes: (attrs: Record<string, unknown>) => void;
455
464
  /** Delete this node from the document. */
@@ -460,7 +469,7 @@ interface ReactNodeViewProps {
460
469
  options: Record<string, unknown>;
461
470
  };
462
471
  /** ProseMirror decorations applied to this node. */
463
- decorations: unknown[];
472
+ decorations: readonly unknown[];
464
473
  }
465
474
  interface ReactNodeViewRendererOptions {
466
475
  /** Wrapper element tag. @default 'div' for block, 'span' for inline */
@@ -493,12 +502,12 @@ interface ReactNodeViewRendererOptions {
493
502
  * }
494
503
  * ```
495
504
  */
496
- declare function ReactNodeViewRenderer(component: React.ComponentType<ReactNodeViewProps>, options?: ReactNodeViewRendererOptions): (node: PMNode, view: unknown, getPos: () => number, decorations: unknown[]) => ReactNodeView;
505
+ declare function ReactNodeViewRenderer(component: React.ComponentType<ReactNodeViewProps>, options?: ReactNodeViewRendererOptions): (node: PMNode, view: unknown, getPos: () => number | undefined, decorations: readonly unknown[]) => ReactNodeView;
497
506
  interface ReactNodeViewInit {
498
507
  editor: Editor;
499
508
  node: PMNode;
500
- getPos: () => number;
501
- decorations: unknown[];
509
+ getPos: () => number | undefined;
510
+ decorations: readonly unknown[];
502
511
  extension: {
503
512
  name: string;
504
513
  options: Record<string, unknown>;
@@ -517,11 +526,14 @@ declare class ReactNodeView {
517
526
  private selected;
518
527
  constructor(component: React.ComponentType<ReactNodeViewProps>, init: ReactNodeViewInit, options: ReactNodeViewRendererOptions);
519
528
  private render;
520
- update(node: PMNode, decorations: unknown[]): boolean;
529
+ update(node: PMNode, decorations: readonly unknown[]): boolean;
521
530
  selectNode(): void;
522
531
  deselectNode(): void;
523
532
  destroy(): void;
524
- ignoreMutation(mutation: MutationRecord): boolean;
533
+ ignoreMutation(mutation: MutationRecord | {
534
+ type: 'selection';
535
+ target: Node;
536
+ }): boolean;
525
537
  stopEvent(): boolean;
526
538
  }
527
539
 
package/dist/index.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import { createContext, forwardRef, useImperativeHandle, useRef, useEffect, useState, useMemo, useCallback, useSyncExternalStore, useContext, Fragment, useLayoutEffect, createElement } from 'react';
2
- import { Editor, Document, Paragraph, Text, BaseKeymap, History, positionFloatingOnce, PluginKey, FloatingMenuController, defaultIcons, positionFloating, ToolbarController, defaultBubbleContexts, createBubbleMenuPlugin } from '@domternal/core';
2
+ import { Editor, Document, Paragraph, Text, BaseKeymap, History, positionFloatingOnce, PluginKey, createFloatingMenuPlugin, FloatingMenuController, defaultIcons, positionFloating, ToolbarController, defaultBubbleContexts, createBubbleMenuPlugin } from '@domternal/core';
3
3
  export { Editor, generateHTML, generateJSON, generateText } from '@domternal/core';
4
4
  import { jsxs, jsx, Fragment as Fragment$1 } from 'react/jsx-runtime';
5
- import { createFloatingMenuPlugin } from '@domternal/extension-block-menu';
6
5
  import { createPortal } from 'react-dom';
7
6
  import { createRoot } from 'react-dom/client';
8
7
 
@@ -14,9 +13,9 @@ function useEditor(options = {}, deps) {
14
13
  content = "",
15
14
  editable = true,
16
15
  autofocus = false,
17
- outputFormat = "html"
16
+ outputFormat = "html",
17
+ immediatelyRender = false
18
18
  } = options;
19
- const [editor, setEditor] = useState(null);
20
19
  const editorRef = useRef(null);
21
20
  const instanceRef = useRef(null);
22
21
  const pendingContentRef = useRef(null);
@@ -45,7 +44,7 @@ function useEditor(options = {}, deps) {
45
44
  callbacksRef.current.onBlur?.({ editor: ed, event });
46
45
  });
47
46
  }
48
- function createEditorInstance(element, initialContent, focus) {
47
+ function buildEditorInstance(element, initialContent, focus) {
49
48
  const ed = new Editor({
50
49
  element,
51
50
  extensions: [...DEFAULT_EXTENSIONS, ...extensions],
@@ -57,6 +56,10 @@ function useEditor(options = {}, deps) {
57
56
  instanceRef.current = ed;
58
57
  extensionsRef.current = extensions;
59
58
  depsRef.current = deps;
59
+ return ed;
60
+ }
61
+ function createEditorInstance(element, initialContent, focus) {
62
+ const ed = buildEditorInstance(element, initialContent, focus);
60
63
  setEditor(ed);
61
64
  callbacksRef.current.onCreate?.(ed);
62
65
  return ed;
@@ -71,7 +74,30 @@ function useEditor(options = {}, deps) {
71
74
  instanceRef.current = null;
72
75
  setEditor(null);
73
76
  }
77
+ const [editor, setEditor] = useState(() => {
78
+ if (!immediatelyRender) return null;
79
+ if (typeof window === "undefined") {
80
+ throw new Error(
81
+ "[@domternal/react] immediatelyRender: true creates the editor during render, which cannot work during server-side rendering. Remove the option for SSR; the editor is then created after mount."
82
+ );
83
+ }
84
+ if (instanceRef.current && !instanceRef.current.isDestroyed) {
85
+ return instanceRef.current;
86
+ }
87
+ return buildEditorInstance(document.createElement("div"), content, autofocus);
88
+ });
74
89
  useEffect(() => {
90
+ const existing = instanceRef.current;
91
+ if (existing && !existing.isDestroyed) {
92
+ const mount = editorRef.current;
93
+ if (mount && existing.view.dom.parentElement !== mount) {
94
+ mount.appendChild(existing.view.dom);
95
+ }
96
+ callbacksRef.current.onCreate?.(existing);
97
+ return () => {
98
+ destroyCurrentEditor();
99
+ };
100
+ }
75
101
  const element = editorRef.current ?? document.createElement("div");
76
102
  const initialContent = pendingContentRef.current ?? content;
77
103
  pendingContentRef.current = null;
@@ -2621,12 +2647,14 @@ var ReactNodeView = class {
2621
2647
  decorations: this.decorations,
2622
2648
  updateAttributes: (attrs) => {
2623
2649
  const pos = this.getPos();
2650
+ if (pos === void 0) return;
2624
2651
  const { tr } = this.editor.view.state;
2625
2652
  tr.setNodeMarkup(pos, void 0, { ...this.node.attrs, ...attrs });
2626
2653
  this.editor.view.dispatch(tr);
2627
2654
  },
2628
2655
  deleteNode: () => {
2629
2656
  const pos = this.getPos();
2657
+ if (pos === void 0) return;
2630
2658
  const { tr } = this.editor.view.state;
2631
2659
  tr.delete(pos, pos + this.node.nodeSize);
2632
2660
  this.editor.view.dispatch(tr);