@domternal/react 0.8.0 → 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 {
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;