@byline/richtext-lexical 2.3.3 → 2.4.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.
@@ -16,6 +16,7 @@ import type { EditorConfig } from './field/config/types';
16
16
  export type LexicalEditorConfigureInput = Omit<EditorConfig, 'extensions'> & {
17
17
  extensions: ExtensionsList;
18
18
  };
19
+ type ConfigureFn = (config: LexicalEditorConfigureInput) => LexicalEditorConfigureInput;
19
20
  /**
20
21
  * Returns a `RichTextEditorComponent` with editor settings baked in. Use
21
22
  * this at the registration site in your admin config when you want to
@@ -23,6 +24,13 @@ export type LexicalEditorConfigureInput = Omit<EditorConfig, 'extensions'> & {
23
24
  * overrides via `RichTextField.editorConfig` continue to take precedence
24
25
  * at render time.
25
26
  *
27
+ * The returned component is lazy: the editor module graph (RichTextField
28
+ * + every built-in extension + the Lexical core) is dynamically imported
29
+ * on first mount, so callers that merely *reference* `lexicalEditor` at
30
+ * registration time don't drag the editor onto every bundle that touches
31
+ * the registration. The `configure` callback runs once the chunk has
32
+ * loaded, with the same seed it received before.
33
+ *
26
34
  * The `configure` callback receives a deep clone of `defaultEditorConfig`
27
35
  * with `extensions` populated from `defaultExtensionsList()`. Mutate the
28
36
  * clone freely — it's local to this call. Use the chainable
@@ -49,4 +57,5 @@ export type LexicalEditorConfigureInput = Omit<EditorConfig, 'extensions'> & {
49
57
  * })
50
58
  * ```
51
59
  */
52
- export declare function lexicalEditor(configure?: (config: LexicalEditorConfigureInput) => LexicalEditorConfigureInput): RichTextEditorComponent;
60
+ export declare function lexicalEditor(configure?: ConfigureFn): RichTextEditorComponent;
61
+ export {};
@@ -1,21 +1,60 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { cloneDeep } from "lodash-es";
3
- import { defaultEditorConfig } from "./field/config/default.js";
4
- import { defaultExtensionsList } from "./field/config/default-extensions.js";
5
- import { RichTextField } from "./richtext-field.js";
2
+ import { Suspense, lazy } from "react";
3
+ import { Shimmer } from "@byline/ui/react";
4
+ let editorBundlePromise = null;
5
+ function loadEditorBundle() {
6
+ if (!editorBundlePromise) editorBundlePromise = (async ()=>{
7
+ const [richtextMod, defaultMod, extensionsMod, lodashMod] = await Promise.all([
8
+ import("./richtext-field.js"),
9
+ import("./field/config/default.js"),
10
+ import("./field/config/default-extensions.js"),
11
+ import("lodash-es")
12
+ ]);
13
+ return {
14
+ RichTextField: richtextMod.RichTextField,
15
+ defaultEditorConfig: defaultMod.defaultEditorConfig,
16
+ defaultExtensionsList: extensionsMod.defaultExtensionsList,
17
+ cloneDeep: lodashMod.cloneDeep
18
+ };
19
+ })();
20
+ return editorBundlePromise;
21
+ }
6
22
  function lexicalEditor(configure) {
7
- let baked;
8
- if (configure) {
9
- const seed = {
10
- ...cloneDeep(defaultEditorConfig),
11
- extensions: defaultExtensionsList()
23
+ const Lazy = /*#__PURE__*/ lazy(async ()=>{
24
+ const { RichTextField, defaultEditorConfig, defaultExtensionsList, cloneDeep } = await loadEditorBundle();
25
+ let baked;
26
+ if (configure) {
27
+ const seed = {
28
+ ...cloneDeep(defaultEditorConfig),
29
+ extensions: defaultExtensionsList()
30
+ };
31
+ baked = configure(seed);
32
+ }
33
+ const Configured = (props)=>/*#__PURE__*/ jsx(RichTextField, {
34
+ ...props,
35
+ editorConfig: baked
36
+ });
37
+ return {
38
+ default: Configured
12
39
  };
13
- baked = configure(seed);
14
- }
15
- const ConfiguredEditor = (props)=>/*#__PURE__*/ jsx(RichTextField, {
16
- ...props,
17
- editorConfig: baked
40
+ });
41
+ const ConfiguredEditor = (props)=>/*#__PURE__*/ jsx(Suspense, {
42
+ fallback: /*#__PURE__*/ jsx(EditorPlaceholder, {}),
43
+ children: /*#__PURE__*/ jsx(Lazy, {
44
+ ...props
45
+ })
18
46
  });
19
47
  return ConfiguredEditor;
20
48
  }
49
+ function EditorPlaceholder() {
50
+ return /*#__PURE__*/ jsx("div", {
51
+ className: "byline-field-richtext",
52
+ children: /*#__PURE__*/ jsx("div", {
53
+ className: "byline-field-richtext-body",
54
+ children: /*#__PURE__*/ jsx(Shimmer, {
55
+ height: "35vh"
56
+ })
57
+ })
58
+ });
59
+ }
21
60
  export { lexicalEditor };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "private": false,
4
4
  "type": "module",
5
5
  "license": "MPL-2.0",
6
- "version": "2.3.3",
6
+ "version": "2.4.0",
7
7
  "engines": {
8
8
  "node": ">=20.9.0"
9
9
  },
@@ -72,9 +72,9 @@
72
72
  "npm-run-all": "^4.1.5",
73
73
  "prism-react-renderer": "^2.4.1",
74
74
  "react-error-boundary": "^6.1.1",
75
- "@byline/core": "2.3.3",
76
- "@byline/client": "2.3.3",
77
- "@byline/ui": "2.3.3"
75
+ "@byline/core": "2.4.0",
76
+ "@byline/client": "2.4.0",
77
+ "@byline/ui": "2.4.0"
78
78
  },
79
79
  "peerDependencies": {
80
80
  "react": "^19.0.0",
@@ -6,12 +6,11 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
 
9
+ import { lazy, Suspense } from 'react'
10
+
9
11
  import type { RichTextEditorComponent } from '@byline/core'
10
- import { cloneDeep } from 'lodash-es'
12
+ import { Shimmer } from '@byline/ui/react'
11
13
 
12
- import { defaultEditorConfig } from './field/config/default'
13
- import { defaultExtensionsList } from './field/config/default-extensions'
14
- import { RichTextField } from './richtext-field'
15
14
  import type { ExtensionsList } from './field/config/extensions-list'
16
15
  import type { EditorConfig } from './field/config/types'
17
16
 
@@ -24,6 +23,46 @@ export type LexicalEditorConfigureInput = Omit<EditorConfig, 'extensions'> & {
24
23
  extensions: ExtensionsList
25
24
  }
26
25
 
26
+ type ConfigureFn = (config: LexicalEditorConfigureInput) => LexicalEditorConfigureInput
27
+
28
+ /**
29
+ * Bundle of editor internals that are dynamically imported on first
30
+ * mount. Kept narrow so the chunk only carries what the configure step
31
+ * + render need — extension classes the user references directly stay
32
+ * out of this bundle and remain tree-shakeable.
33
+ */
34
+ interface EditorBundle {
35
+ RichTextField: typeof import('./richtext-field').RichTextField
36
+ defaultEditorConfig: typeof import('./field/config/default').defaultEditorConfig
37
+ defaultExtensionsList: typeof import('./field/config/default-extensions').defaultExtensionsList
38
+ cloneDeep: <T>(value: T) => T
39
+ }
40
+
41
+ let editorBundlePromise: Promise<EditorBundle> | null = null
42
+
43
+ function loadEditorBundle(): Promise<EditorBundle> {
44
+ // Memoize the import so multiple `lexicalEditor()` calls share one chunk
45
+ // load — React.lazy already caches per-wrapper, but consumers that call
46
+ // the factory more than once would otherwise create parallel promises.
47
+ if (!editorBundlePromise) {
48
+ editorBundlePromise = (async () => {
49
+ const [richtextMod, defaultMod, extensionsMod, lodashMod] = await Promise.all([
50
+ import('./richtext-field'),
51
+ import('./field/config/default'),
52
+ import('./field/config/default-extensions'),
53
+ import('lodash-es'),
54
+ ])
55
+ return {
56
+ RichTextField: richtextMod.RichTextField,
57
+ defaultEditorConfig: defaultMod.defaultEditorConfig,
58
+ defaultExtensionsList: extensionsMod.defaultExtensionsList,
59
+ cloneDeep: lodashMod.cloneDeep,
60
+ }
61
+ })()
62
+ }
63
+ return editorBundlePromise
64
+ }
65
+
27
66
  /**
28
67
  * Returns a `RichTextEditorComponent` with editor settings baked in. Use
29
68
  * this at the registration site in your admin config when you want to
@@ -31,6 +70,13 @@ export type LexicalEditorConfigureInput = Omit<EditorConfig, 'extensions'> & {
31
70
  * overrides via `RichTextField.editorConfig` continue to take precedence
32
71
  * at render time.
33
72
  *
73
+ * The returned component is lazy: the editor module graph (RichTextField
74
+ * + every built-in extension + the Lexical core) is dynamically imported
75
+ * on first mount, so callers that merely *reference* `lexicalEditor` at
76
+ * registration time don't drag the editor onto every bundle that touches
77
+ * the registration. The `configure` callback runs once the chunk has
78
+ * loaded, with the same seed it received before.
79
+ *
34
80
  * The `configure` callback receives a deep clone of `defaultEditorConfig`
35
81
  * with `extensions` populated from `defaultExtensionsList()`. Mutate the
36
82
  * clone freely — it's local to this call. Use the chainable
@@ -57,21 +103,47 @@ export type LexicalEditorConfigureInput = Omit<EditorConfig, 'extensions'> & {
57
103
  * })
58
104
  * ```
59
105
  */
60
- export function lexicalEditor(
61
- configure?: (config: LexicalEditorConfigureInput) => LexicalEditorConfigureInput
62
- ): RichTextEditorComponent {
63
- let baked: EditorConfig | undefined
64
- if (configure) {
65
- const seed: LexicalEditorConfigureInput = {
66
- ...cloneDeep(defaultEditorConfig),
67
- extensions: defaultExtensionsList(),
106
+ export function lexicalEditor(configure?: ConfigureFn): RichTextEditorComponent {
107
+ const Lazy = lazy(async () => {
108
+ const { RichTextField, defaultEditorConfig, defaultExtensionsList, cloneDeep } =
109
+ await loadEditorBundle()
110
+
111
+ let baked: EditorConfig | undefined
112
+ if (configure) {
113
+ const seed: LexicalEditorConfigureInput = {
114
+ ...cloneDeep(defaultEditorConfig),
115
+ extensions: defaultExtensionsList(),
116
+ }
117
+ baked = configure(seed)
68
118
  }
69
- baked = configure(seed)
70
- }
119
+
120
+ const Configured: RichTextEditorComponent = (props) => (
121
+ <RichTextField {...props} editorConfig={baked} />
122
+ )
123
+ return { default: Configured as React.ComponentType<any> }
124
+ })
71
125
 
72
126
  const ConfiguredEditor: RichTextEditorComponent = (props) => (
73
- <RichTextField {...props} editorConfig={baked} />
127
+ <Suspense fallback={<EditorPlaceholder />}>
128
+ <Lazy {...(props as object)} />
129
+ </Suspense>
74
130
  )
75
-
76
131
  return ConfiguredEditor
77
132
  }
133
+
134
+ /**
135
+ * Skeleton shown while the editor module graph is loading. Mirrors the
136
+ * `byline-field-richtext` / `byline-field-richtext-body` shell that
137
+ * `RichTextField` renders, and reuses the same `Shimmer` placeholder the
138
+ * inner `EditorField` Suspense uses — so the visible cold-load sequence
139
+ * is just "shimmer → editor" instead of "blank → shimmer → editor".
140
+ */
141
+ function EditorPlaceholder() {
142
+ return (
143
+ <div className="byline-field-richtext">
144
+ <div className="byline-field-richtext-body">
145
+ <Shimmer height="35vh" />
146
+ </div>
147
+ </div>
148
+ )
149
+ }