@astryxdesign/richtext 0.6.0-canary.f574abd → 0.6.1-canary.09decfb
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/RichTextEditor.d.ts +3 -2
- package/dist/RichTextEditor.d.ts.map +1 -1
- package/dist/RichTextEditor.js +38 -17
- package/dist/RichTextView.d.ts +2 -1
- package/dist/RichTextView.d.ts.map +1 -1
- package/dist/RichTextView.js +43 -28
- package/package.json +9 -3
- package/src/RichTextEditor.tsx +39 -19
- package/src/RichTextView.tsx +50 -32
package/dist/RichTextEditor.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file RichTextEditor.tsx
|
|
3
|
-
* @input Uses React, useId, Lexical (lexical + @lexical/react
|
|
4
|
-
*
|
|
3
|
+
* @input Uses React, useId, Lexical (lexical + @lexical/react, composed through
|
|
4
|
+
* LexicalExtensionComposer), Field, VisuallyHidden, useInputStatusIcon,
|
|
5
|
+
* mergeProps, design tokens
|
|
5
6
|
* @output Exports an accessibly labelled RichTextEditor component with a flush
|
|
6
7
|
* top toolbar slot and configurable editable-surface minimum height, RichTextEditorProps,
|
|
7
8
|
* RichTextEditorStatus, RichTextEditorStatusType, RichTextEditorSize
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RichTextEditor.d.ts","sourceRoot":"","sources":["../src/RichTextEditor.tsx"],"names":[],"mappings":"AAIA
|
|
1
|
+
{"version":3,"file":"RichTextEditor.d.ts","sourceRoot":"","sources":["../src/RichTextEditor.tsx"],"names":[],"mappings":"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"}
|
package/dist/RichTextEditor.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file RichTextEditor.tsx
|
|
7
|
-
* @input Uses React, useId, Lexical (lexical + @lexical/react
|
|
8
|
-
*
|
|
7
|
+
* @input Uses React, useId, Lexical (lexical + @lexical/react, composed through
|
|
8
|
+
* LexicalExtensionComposer), Field, VisuallyHidden, useInputStatusIcon,
|
|
9
|
+
* mergeProps, design tokens
|
|
9
10
|
* @output Exports an accessibly labelled RichTextEditor component with a flush
|
|
10
11
|
* top toolbar slot and configurable editable-surface minimum height, RichTextEditorProps,
|
|
11
12
|
* RichTextEditorStatus, RichTextEditorStatusType, RichTextEditorSize
|
|
@@ -34,7 +35,7 @@ import { useInputStatusIcon } from '@astryxdesign/core/hooks';
|
|
|
34
35
|
import { VisuallyHidden } from '@astryxdesign/core/VisuallyHidden';
|
|
35
36
|
import { mergeProps, themeProps } from '@astryxdesign/core/utils';
|
|
36
37
|
import { useSize } from '@astryxdesign/core/SizeContext';
|
|
37
|
-
import {
|
|
38
|
+
import { LexicalExtensionComposer } from '@lexical/react/LexicalExtensionComposer';
|
|
38
39
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
39
40
|
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
|
40
41
|
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
|
@@ -48,7 +49,7 @@ import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
|
|
|
48
49
|
import { TRANSFORMERS, $convertToMarkdownString } from '@lexical/markdown';
|
|
49
50
|
import { $generateHtmlFromNodes } from '@lexical/html';
|
|
50
51
|
import { DEFAULT_NODES } from "./editorNodes.js";
|
|
51
|
-
import { BLUR_COMMAND, COMMAND_PRIORITY_LOW, KEY_DOWN_COMMAND, KEY_ESCAPE_COMMAND, KEY_TAB_COMMAND, mergeRegister } from 'lexical';
|
|
52
|
+
import { BLUR_COMMAND, COMMAND_PRIORITY_LOW, defineExtension, KEY_DOWN_COMMAND, KEY_ESCAPE_COMMAND, KEY_TAB_COMMAND, mergeRegister } from 'lexical';
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
55
|
* Serialized state for an empty editor: a root containing a single empty
|
|
@@ -248,17 +249,36 @@ export const RichTextEditor = /*#__PURE__*/forwardRef(function RichTextEditor({
|
|
|
248
249
|
// array each time; memoize on the prop identity instead. (React Compiler
|
|
249
250
|
// isn't running the transform in this repo, so this isn't auto-memoized.)
|
|
250
251
|
const markdownTransformers = useMemo(() => [...transformers], [transformers]);
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
252
|
+
|
|
253
|
+
// The extension replaces LexicalComposer's `initialConfig`: it carries the
|
|
254
|
+
// same editor configuration (namespace, theme, nodes, editability, initial
|
|
255
|
+
// state) in the shape LexicalBuilder consumes.
|
|
256
|
+
//
|
|
257
|
+
// LexicalExtensionComposer re-creates the editor whenever the extension's
|
|
258
|
+
// identity changes, whereas LexicalComposer read `initialConfig` exactly once
|
|
259
|
+
// on mount. Build it on first render and keep it, so the editor's lifetime —
|
|
260
|
+
// and the content it holds — is unaffected by later renders (a consumer
|
|
261
|
+
// passing an inline `nodes={[...]}` array would otherwise blow away the
|
|
262
|
+
// editor's content on every render).
|
|
263
|
+
const extensionRef = useRef(null);
|
|
264
|
+
if (extensionRef.current === null) {
|
|
265
|
+
extensionRef.current = defineExtension({
|
|
266
|
+
name: '@astryxdesign/richtext/RichTextEditor',
|
|
267
|
+
namespace,
|
|
268
|
+
theme: themeRef.current,
|
|
269
|
+
editable,
|
|
270
|
+
nodes: nodes ? [...DEFAULT_NODES, ...nodes] : [...DEFAULT_NODES],
|
|
271
|
+
// `undefined` (not `null`) leaves Lexical's default initializer in place,
|
|
272
|
+
// which seeds the empty document with one paragraph — what
|
|
273
|
+
// LexicalComposer did when no `editorState` was given. `null` would mean
|
|
274
|
+
// "start from a genuinely empty root".
|
|
275
|
+
$initialEditorState: defaultValue ?? undefined,
|
|
276
|
+
onError(error) {
|
|
277
|
+
// Surface errors to the host app rather than swallowing them.
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
262
282
|
const hasTabEscapeHint = editable && tabEscapeHint !== '';
|
|
263
283
|
const hasToolbar = toolbar != null && typeof toolbar !== 'boolean';
|
|
264
284
|
const {
|
|
@@ -295,8 +315,9 @@ export const RichTextEditor = /*#__PURE__*/forwardRef(function RichTextEditor({
|
|
|
295
315
|
size,
|
|
296
316
|
status: status?.type ?? null
|
|
297
317
|
}), stylex.props(inputWrapperStyles.base, styles.wrapper, (isDisabled || isReadOnly) && inputWrapperStyles.disabled, isDisabled && styles.disabled, status && inputStatusBorderStyles[status.type], status && !isDisabled && !isReadOnly && inputStatusHoverShadowStyles[status.type], status && inputStatusFocusWithinStyles[status.type], xstyle), className, style),
|
|
298
|
-
children: [/*#__PURE__*/_jsxs(
|
|
299
|
-
|
|
318
|
+
children: [/*#__PURE__*/_jsxs(LexicalExtensionComposer, {
|
|
319
|
+
extension: extensionRef.current,
|
|
320
|
+
contentEditable: null,
|
|
300
321
|
children: [hasToolbar ? toolbar : null, /*#__PURE__*/_jsxs("div", {
|
|
301
322
|
...stylex.props(styles.editorBody, hasToolbar && styles.editorBodyWithToolbar, !!statusIcon && styles.editorBodyWithStatus, editorBodySizeStyles[size]),
|
|
302
323
|
children: [/*#__PURE__*/_jsxs("div", {
|
package/dist/RichTextView.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file RichTextView.tsx
|
|
3
|
-
* @input Uses React, Lexical (lexical + @lexical/react
|
|
3
|
+
* @input Uses React, Lexical (lexical + @lexical/react, composed through
|
|
4
|
+
* LexicalExtensionComposer), design tokens
|
|
4
5
|
* @output Exports RichTextView component and RichTextViewProps
|
|
5
6
|
* @position Read-only renderer for serialized Lexical editor state; experimental
|
|
6
7
|
* (richtext), exported from @astryxdesign/richtext
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RichTextView.d.ts","sourceRoot":"","sources":["../src/RichTextView.tsx"],"names":[],"mappings":"AAIA
|
|
1
|
+
{"version":3,"file":"RichTextView.d.ts","sourceRoot":"","sources":["../src/RichTextView.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAA8B,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAGlE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAWlD,OAAO,KAAK,EAEV,KAAK,EACL,WAAW,EAEZ,MAAM,SAAS,CAAC;AAoBjB,MAAM,WAAW,iBAAkB,SAAQ,SAAS;IAClD;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;IAC1C;;;OAGG;IACH,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACtC;;;;OAIG;IACH,aAAa,CAAC,EAAE,SAAS,CAAC;CAC3B;AAyCD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,KAAK,EACL,OAAO,EACP,SAAyB,EACzB,YAAY,EACZ,aAAoB,EACpB,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,IAAI,EACR,EAAE,iBAAiB,+BAyFnB;yBApGe,YAAY"}
|
package/dist/RichTextView.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file RichTextView.tsx
|
|
7
|
-
* @input Uses React, Lexical (lexical + @lexical/react
|
|
7
|
+
* @input Uses React, Lexical (lexical + @lexical/react, composed through
|
|
8
|
+
* LexicalExtensionComposer), design tokens
|
|
8
9
|
* @output Exports RichTextView component and RichTextViewProps
|
|
9
10
|
* @position Read-only renderer for serialized Lexical editor state; experimental
|
|
10
11
|
* (richtext), exported from @astryxdesign/richtext
|
|
@@ -17,7 +18,7 @@
|
|
|
17
18
|
import { useEffect, useRef, useState } from 'react';
|
|
18
19
|
import * as stylex from '@stylexjs/stylex';
|
|
19
20
|
import { sharedEditorTheme } from "./editorTheme.js";
|
|
20
|
-
import {
|
|
21
|
+
import { LexicalExtensionComposer } from '@lexical/react/LexicalExtensionComposer';
|
|
21
22
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
22
23
|
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
|
23
24
|
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
|
@@ -26,6 +27,7 @@ import { ListNode, ListItemNode } from '@lexical/list';
|
|
|
26
27
|
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
|
27
28
|
import { LinkNode, AutoLinkNode } from '@lexical/link';
|
|
28
29
|
import { CodeNode, CodeHighlightNode } from '@lexical/code';
|
|
30
|
+
import { defineExtension } from 'lexical';
|
|
29
31
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
30
32
|
const styles = {
|
|
31
33
|
root: {
|
|
@@ -37,30 +39,28 @@ const DEFAULT_NODES = [HeadingNode, QuoteNode, ListNode, ListItemNode, LinkNode,
|
|
|
37
39
|
/**
|
|
38
40
|
* Keeps the rendered content in sync with the `value` prop after mount.
|
|
39
41
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
+
* The editor's initial state is seeded once, when the composer mounts, so a
|
|
43
|
+
* plain `<RichTextView value={changingValue} />` would freeze at its first
|
|
42
44
|
* value — the content would never update when `value` changed. This plugin runs
|
|
43
45
|
* inside the composer context and re-applies `value` whenever it changes, so the
|
|
44
46
|
* read-only view stays reactive (e.g. previewing content edited elsewhere).
|
|
45
47
|
*
|
|
46
|
-
* The initial `value` is already applied via
|
|
47
|
-
* skip the first run to avoid a redundant re-parse
|
|
48
|
+
* The initial `value` is already applied via the extension's
|
|
49
|
+
* `$initialEditorState`, so we skip the first run to avoid a redundant re-parse
|
|
50
|
+
* on mount.
|
|
48
51
|
*
|
|
49
52
|
* This mirrors the canonical Lexical pattern for applying externally-sourced
|
|
50
53
|
* serialized state after mount: the Lexical Playground's ActionsPlugin does the
|
|
51
54
|
* same `editor.setEditorState(editor.parseEditorState(...))` from inside a
|
|
52
55
|
* plugin (see facebook/lexical
|
|
53
56
|
* packages/lexical-playground/src/plugins/ActionsPlugin/index.tsx). It is
|
|
54
|
-
* necessary because
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* Playground's accompanying CLEAR_HISTORY_COMMAND.
|
|
57
|
+
* necessary because the composer builds the editor once and reads the initial
|
|
58
|
+
* state exactly once at that point, so a changed prop cannot re-seed the editor
|
|
59
|
+
* on its own. A read-only view has no history, so we skip the Playground's
|
|
60
|
+
* accompanying CLEAR_HISTORY_COMMAND.
|
|
59
61
|
*
|
|
60
62
|
* `parseEditorState` / `setEditorState` are methods on the editor instance, so
|
|
61
|
-
* this
|
|
62
|
-
* sandbox's Next build to transpile lexical's raw `src/*.ts` and fail — see the
|
|
63
|
-
* matching note in RichTextEditor.tsx).
|
|
63
|
+
* this plugin needs no module-level Lexical imports of its own.
|
|
64
64
|
*/
|
|
65
65
|
function SyncValuePlugin({
|
|
66
66
|
value
|
|
@@ -103,6 +103,12 @@ export function RichTextView({
|
|
|
103
103
|
if (themeRef.current === null) {
|
|
104
104
|
themeRef.current = sharedEditorTheme();
|
|
105
105
|
}
|
|
106
|
+
|
|
107
|
+
// Built on first render (and again after an error, below) rather than per
|
|
108
|
+
// render: LexicalExtensionComposer re-creates the editor whenever the
|
|
109
|
+
// extension's identity changes, and `value` updates are applied in place by
|
|
110
|
+
// SyncValuePlugin instead.
|
|
111
|
+
const extensionRef = useRef(null);
|
|
106
112
|
const [hasError, setHasError] = useState(false);
|
|
107
113
|
|
|
108
114
|
// Reset the error state when the value changes so a corrected value recovers.
|
|
@@ -119,8 +125,9 @@ export function RichTextView({
|
|
|
119
125
|
};
|
|
120
126
|
|
|
121
127
|
// Validate `value` parses as JSON before handing it to Lexical. Malformed
|
|
122
|
-
// JSON would otherwise throw synchronously
|
|
123
|
-
// escape any error boundary, taking down the host on the render
|
|
128
|
+
// JSON would otherwise throw synchronously while the composer builds the
|
|
129
|
+
// editor and escape any error boundary, taking down the host on the render
|
|
130
|
+
// path.
|
|
124
131
|
if (!hasError) {
|
|
125
132
|
try {
|
|
126
133
|
JSON.parse(value);
|
|
@@ -129,6 +136,10 @@ export function RichTextView({
|
|
|
129
136
|
}
|
|
130
137
|
}
|
|
131
138
|
if (hasError) {
|
|
139
|
+
// The composer subtree is unmounted while the fallback renders. Drop the
|
|
140
|
+
// extension so that recovering from the error builds a fresh one, seeded
|
|
141
|
+
// with the corrected `value`.
|
|
142
|
+
extensionRef.current = null;
|
|
132
143
|
return /*#__PURE__*/_jsx("div", {
|
|
133
144
|
...stylex.props(styles.root, xstyle),
|
|
134
145
|
className: className,
|
|
@@ -137,23 +148,27 @@ export function RichTextView({
|
|
|
137
148
|
children: errorFallback
|
|
138
149
|
});
|
|
139
150
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
151
|
+
if (extensionRef.current === null) {
|
|
152
|
+
extensionRef.current = defineExtension({
|
|
153
|
+
name: '@astryxdesign/richtext/RichTextView',
|
|
154
|
+
namespace,
|
|
155
|
+
theme: themeRef.current,
|
|
156
|
+
editable: false,
|
|
157
|
+
nodes: nodes ? [...DEFAULT_NODES, ...nodes] : [...DEFAULT_NODES],
|
|
158
|
+
$initialEditorState: value,
|
|
159
|
+
// A read-only view renders persisted content; a bad node/schema should not
|
|
160
|
+
// crash the host. Surface it via onParseError + fallback instead of re-throwing.
|
|
161
|
+
onError: handleError
|
|
162
|
+
});
|
|
163
|
+
}
|
|
150
164
|
return /*#__PURE__*/_jsx("div", {
|
|
151
165
|
...stylex.props(styles.root, xstyle),
|
|
152
166
|
className: className,
|
|
153
167
|
style: style,
|
|
154
168
|
...rest,
|
|
155
|
-
children: /*#__PURE__*/_jsxs(
|
|
156
|
-
|
|
169
|
+
children: /*#__PURE__*/_jsxs(LexicalExtensionComposer, {
|
|
170
|
+
extension: extensionRef.current,
|
|
171
|
+
contentEditable: null,
|
|
157
172
|
children: [/*#__PURE__*/_jsx(SyncValuePlugin, {
|
|
158
173
|
value: value
|
|
159
174
|
}), /*#__PURE__*/_jsx(RichTextPlugin, {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/richtext",
|
|
3
3
|
"displayName": "Rich Text",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.1-canary.09decfb",
|
|
5
5
|
"description": "Astryx rich text — a Lexical-based rich text editor and viewer. Published to npm only under the @canary dist-tag for early testing; never released as a stable (latest) version.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
7
7
|
"license": "MIT",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"@stylexjs/stylex": ">=0.10.0",
|
|
53
|
-
"@astryxdesign/core": "0.6.
|
|
53
|
+
"@astryxdesign/core": "0.6.1-canary.09decfb",
|
|
54
54
|
"react": ">=19.0.0",
|
|
55
55
|
"react-dom": ">=19.0.0",
|
|
56
56
|
"lexical": "^0.46.0",
|
|
@@ -101,7 +101,11 @@
|
|
|
101
101
|
}
|
|
102
102
|
},
|
|
103
103
|
"devDependencies": {
|
|
104
|
-
"@astryxdesign/cli": "0.6.
|
|
104
|
+
"@astryxdesign/cli": "0.6.1-canary.09decfb",
|
|
105
|
+
"@babel/cli": "^7.29.7",
|
|
106
|
+
"@babel/core": "^7.29.7",
|
|
107
|
+
"@babel/preset-react": "^7.29.7",
|
|
108
|
+
"@babel/preset-typescript": "^7.29.7",
|
|
105
109
|
"@lexical/code": "^0.46.0",
|
|
106
110
|
"@lexical/headless": "^0.46.0",
|
|
107
111
|
"@lexical/html": "^0.46.0",
|
|
@@ -112,6 +116,8 @@
|
|
|
112
116
|
"@lexical/rich-text": "^0.46.0",
|
|
113
117
|
"@lexical/selection": "^0.46.0",
|
|
114
118
|
"@lexical/utils": "^0.46.0",
|
|
119
|
+
"@stylexjs/babel-plugin": "^0.19.0",
|
|
120
|
+
"@testing-library/react": "^16.3.2",
|
|
115
121
|
"lexical": "^0.46.0"
|
|
116
122
|
},
|
|
117
123
|
"scripts": {
|
package/src/RichTextEditor.tsx
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file RichTextEditor.tsx
|
|
7
|
-
* @input Uses React, useId, Lexical (lexical + @lexical/react
|
|
8
|
-
*
|
|
7
|
+
* @input Uses React, useId, Lexical (lexical + @lexical/react, composed through
|
|
8
|
+
* LexicalExtensionComposer), Field, VisuallyHidden, useInputStatusIcon,
|
|
9
|
+
* mergeProps, design tokens
|
|
9
10
|
* @output Exports an accessibly labelled RichTextEditor component with a flush
|
|
10
11
|
* top toolbar slot and configurable editable-surface minimum height, RichTextEditorProps,
|
|
11
12
|
* RichTextEditorStatus, RichTextEditorStatusType, RichTextEditorSize
|
|
@@ -58,10 +59,7 @@ import {VisuallyHidden} from '@astryxdesign/core/VisuallyHidden';
|
|
|
58
59
|
import {mergeProps, themeProps, type SizeValue} from '@astryxdesign/core/utils';
|
|
59
60
|
import {useSize} from '@astryxdesign/core/SizeContext';
|
|
60
61
|
|
|
61
|
-
import {
|
|
62
|
-
LexicalComposer,
|
|
63
|
-
type InitialConfigType,
|
|
64
|
-
} from '@lexical/react/LexicalComposer';
|
|
62
|
+
import {LexicalExtensionComposer} from '@lexical/react/LexicalExtensionComposer';
|
|
65
63
|
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
|
66
64
|
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
|
|
67
65
|
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
|
|
@@ -83,10 +81,12 @@ import {DEFAULT_NODES} from './editorNodes';
|
|
|
83
81
|
import {
|
|
84
82
|
BLUR_COMMAND,
|
|
85
83
|
COMMAND_PRIORITY_LOW,
|
|
84
|
+
defineExtension,
|
|
86
85
|
KEY_DOWN_COMMAND,
|
|
87
86
|
KEY_ESCAPE_COMMAND,
|
|
88
87
|
KEY_TAB_COMMAND,
|
|
89
88
|
mergeRegister,
|
|
89
|
+
type AnyLexicalExtension,
|
|
90
90
|
type EditorState,
|
|
91
91
|
type Klass,
|
|
92
92
|
type LexicalEditor,
|
|
@@ -526,17 +526,35 @@ export const RichTextEditor = forwardRef<
|
|
|
526
526
|
// isn't running the transform in this repo, so this isn't auto-memoized.)
|
|
527
527
|
const markdownTransformers = useMemo(() => [...transformers], [transformers]);
|
|
528
528
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
529
|
+
// The extension replaces LexicalComposer's `initialConfig`: it carries the
|
|
530
|
+
// same editor configuration (namespace, theme, nodes, editability, initial
|
|
531
|
+
// state) in the shape LexicalBuilder consumes.
|
|
532
|
+
//
|
|
533
|
+
// LexicalExtensionComposer re-creates the editor whenever the extension's
|
|
534
|
+
// identity changes, whereas LexicalComposer read `initialConfig` exactly once
|
|
535
|
+
// on mount. Build it on first render and keep it, so the editor's lifetime —
|
|
536
|
+
// and the content it holds — is unaffected by later renders (a consumer
|
|
537
|
+
// passing an inline `nodes={[...]}` array would otherwise blow away the
|
|
538
|
+
// editor's content on every render).
|
|
539
|
+
const extensionRef = useRef<AnyLexicalExtension | null>(null);
|
|
540
|
+
if (extensionRef.current === null) {
|
|
541
|
+
extensionRef.current = defineExtension({
|
|
542
|
+
name: '@astryxdesign/richtext/RichTextEditor',
|
|
543
|
+
namespace,
|
|
544
|
+
theme: themeRef.current,
|
|
545
|
+
editable,
|
|
546
|
+
nodes: nodes ? [...DEFAULT_NODES, ...nodes] : [...DEFAULT_NODES],
|
|
547
|
+
// `undefined` (not `null`) leaves Lexical's default initializer in place,
|
|
548
|
+
// which seeds the empty document with one paragraph — what
|
|
549
|
+
// LexicalComposer did when no `editorState` was given. `null` would mean
|
|
550
|
+
// "start from a genuinely empty root".
|
|
551
|
+
$initialEditorState: defaultValue ?? undefined,
|
|
552
|
+
onError(error: Error) {
|
|
553
|
+
// Surface errors to the host app rather than swallowing them.
|
|
554
|
+
throw error;
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
}
|
|
540
558
|
|
|
541
559
|
const hasTabEscapeHint = editable && tabEscapeHint !== '';
|
|
542
560
|
const hasToolbar = toolbar != null && typeof toolbar !== 'boolean';
|
|
@@ -606,7 +624,9 @@ export const RichTextEditor = forwardRef<
|
|
|
606
624
|
className,
|
|
607
625
|
style,
|
|
608
626
|
)}>
|
|
609
|
-
<
|
|
627
|
+
<LexicalExtensionComposer
|
|
628
|
+
extension={extensionRef.current}
|
|
629
|
+
contentEditable={null}>
|
|
610
630
|
{hasToolbar ? toolbar : null}
|
|
611
631
|
<div
|
|
612
632
|
{...stylex.props(
|
|
@@ -664,7 +684,7 @@ export const RichTextEditor = forwardRef<
|
|
|
664
684
|
<div {...stylex.props(styles.statusIcon)}>{statusIcon}</div>
|
|
665
685
|
)}
|
|
666
686
|
</div>
|
|
667
|
-
</
|
|
687
|
+
</LexicalExtensionComposer>
|
|
668
688
|
{hasTabEscapeHint && (
|
|
669
689
|
<VisuallyHidden id={tabEscapeHintID}>{tabEscapeHint}</VisuallyHidden>
|
|
670
690
|
)}
|
package/src/RichTextView.tsx
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file RichTextView.tsx
|
|
7
|
-
* @input Uses React, Lexical (lexical + @lexical/react
|
|
7
|
+
* @input Uses React, Lexical (lexical + @lexical/react, composed through
|
|
8
|
+
* LexicalExtensionComposer), design tokens
|
|
8
9
|
* @output Exports RichTextView component and RichTextViewProps
|
|
9
10
|
* @position Read-only renderer for serialized Lexical editor state; experimental
|
|
10
11
|
* (richtext), exported from @astryxdesign/richtext
|
|
@@ -20,10 +21,7 @@ import * as stylex from '@stylexjs/stylex';
|
|
|
20
21
|
import {sharedEditorTheme} from './editorTheme';
|
|
21
22
|
import type {BaseProps} from '@astryxdesign/core';
|
|
22
23
|
|
|
23
|
-
import {
|
|
24
|
-
LexicalComposer,
|
|
25
|
-
type InitialConfigType,
|
|
26
|
-
} from '@lexical/react/LexicalComposer';
|
|
24
|
+
import {LexicalExtensionComposer} from '@lexical/react/LexicalExtensionComposer';
|
|
27
25
|
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
|
28
26
|
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
|
|
29
27
|
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
|
|
@@ -32,7 +30,13 @@ import {ListNode, ListItemNode} from '@lexical/list';
|
|
|
32
30
|
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
|
|
33
31
|
import {LinkNode, AutoLinkNode} from '@lexical/link';
|
|
34
32
|
import {CodeNode, CodeHighlightNode} from '@lexical/code';
|
|
35
|
-
import type {
|
|
33
|
+
import type {
|
|
34
|
+
AnyLexicalExtension,
|
|
35
|
+
Klass,
|
|
36
|
+
LexicalNode,
|
|
37
|
+
EditorThemeClasses,
|
|
38
|
+
} from 'lexical';
|
|
39
|
+
import {defineExtension} from 'lexical';
|
|
36
40
|
|
|
37
41
|
const styles = stylex.create({
|
|
38
42
|
root: {
|
|
@@ -89,30 +93,28 @@ export interface RichTextViewProps extends BaseProps {
|
|
|
89
93
|
/**
|
|
90
94
|
* Keeps the rendered content in sync with the `value` prop after mount.
|
|
91
95
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
96
|
+
* The editor's initial state is seeded once, when the composer mounts, so a
|
|
97
|
+
* plain `<RichTextView value={changingValue} />` would freeze at its first
|
|
94
98
|
* value — the content would never update when `value` changed. This plugin runs
|
|
95
99
|
* inside the composer context and re-applies `value` whenever it changes, so the
|
|
96
100
|
* read-only view stays reactive (e.g. previewing content edited elsewhere).
|
|
97
101
|
*
|
|
98
|
-
* The initial `value` is already applied via
|
|
99
|
-
* skip the first run to avoid a redundant re-parse
|
|
102
|
+
* The initial `value` is already applied via the extension's
|
|
103
|
+
* `$initialEditorState`, so we skip the first run to avoid a redundant re-parse
|
|
104
|
+
* on mount.
|
|
100
105
|
*
|
|
101
106
|
* This mirrors the canonical Lexical pattern for applying externally-sourced
|
|
102
107
|
* serialized state after mount: the Lexical Playground's ActionsPlugin does the
|
|
103
108
|
* same `editor.setEditorState(editor.parseEditorState(...))` from inside a
|
|
104
109
|
* plugin (see facebook/lexical
|
|
105
110
|
* packages/lexical-playground/src/plugins/ActionsPlugin/index.tsx). It is
|
|
106
|
-
* necessary because
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* Playground's accompanying CLEAR_HISTORY_COMMAND.
|
|
111
|
+
* necessary because the composer builds the editor once and reads the initial
|
|
112
|
+
* state exactly once at that point, so a changed prop cannot re-seed the editor
|
|
113
|
+
* on its own. A read-only view has no history, so we skip the Playground's
|
|
114
|
+
* accompanying CLEAR_HISTORY_COMMAND.
|
|
111
115
|
*
|
|
112
116
|
* `parseEditorState` / `setEditorState` are methods on the editor instance, so
|
|
113
|
-
* this
|
|
114
|
-
* sandbox's Next build to transpile lexical's raw `src/*.ts` and fail — see the
|
|
115
|
-
* matching note in RichTextEditor.tsx).
|
|
117
|
+
* this plugin needs no module-level Lexical imports of its own.
|
|
116
118
|
*/
|
|
117
119
|
function SyncValuePlugin({value}: {value: string}): null {
|
|
118
120
|
const [editor] = useLexicalComposerContext();
|
|
@@ -154,6 +156,12 @@ export function RichTextView({
|
|
|
154
156
|
themeRef.current = sharedEditorTheme();
|
|
155
157
|
}
|
|
156
158
|
|
|
159
|
+
// Built on first render (and again after an error, below) rather than per
|
|
160
|
+
// render: LexicalExtensionComposer re-creates the editor whenever the
|
|
161
|
+
// extension's identity changes, and `value` updates are applied in place by
|
|
162
|
+
// SyncValuePlugin instead.
|
|
163
|
+
const extensionRef = useRef<AnyLexicalExtension | null>(null);
|
|
164
|
+
|
|
157
165
|
const [hasError, setHasError] = useState(false);
|
|
158
166
|
|
|
159
167
|
// Reset the error state when the value changes so a corrected value recovers.
|
|
@@ -171,8 +179,9 @@ export function RichTextView({
|
|
|
171
179
|
};
|
|
172
180
|
|
|
173
181
|
// Validate `value` parses as JSON before handing it to Lexical. Malformed
|
|
174
|
-
// JSON would otherwise throw synchronously
|
|
175
|
-
// escape any error boundary, taking down the host on the render
|
|
182
|
+
// JSON would otherwise throw synchronously while the composer builds the
|
|
183
|
+
// editor and escape any error boundary, taking down the host on the render
|
|
184
|
+
// path.
|
|
176
185
|
if (!hasError) {
|
|
177
186
|
try {
|
|
178
187
|
JSON.parse(value);
|
|
@@ -182,6 +191,10 @@ export function RichTextView({
|
|
|
182
191
|
}
|
|
183
192
|
|
|
184
193
|
if (hasError) {
|
|
194
|
+
// The composer subtree is unmounted while the fallback renders. Drop the
|
|
195
|
+
// extension so that recovering from the error builds a fresh one, seeded
|
|
196
|
+
// with the corrected `value`.
|
|
197
|
+
extensionRef.current = null;
|
|
185
198
|
return (
|
|
186
199
|
<div
|
|
187
200
|
{...stylex.props(styles.root, xstyle)}
|
|
@@ -193,16 +206,19 @@ export function RichTextView({
|
|
|
193
206
|
);
|
|
194
207
|
}
|
|
195
208
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
209
|
+
if (extensionRef.current === null) {
|
|
210
|
+
extensionRef.current = defineExtension({
|
|
211
|
+
name: '@astryxdesign/richtext/RichTextView',
|
|
212
|
+
namespace,
|
|
213
|
+
theme: themeRef.current,
|
|
214
|
+
editable: false,
|
|
215
|
+
nodes: nodes ? [...DEFAULT_NODES, ...nodes] : [...DEFAULT_NODES],
|
|
216
|
+
$initialEditorState: value,
|
|
217
|
+
// A read-only view renders persisted content; a bad node/schema should not
|
|
218
|
+
// crash the host. Surface it via onParseError + fallback instead of re-throwing.
|
|
219
|
+
onError: handleError,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
206
222
|
|
|
207
223
|
return (
|
|
208
224
|
<div
|
|
@@ -210,7 +226,9 @@ export function RichTextView({
|
|
|
210
226
|
className={className}
|
|
211
227
|
style={style}
|
|
212
228
|
{...rest}>
|
|
213
|
-
<
|
|
229
|
+
<LexicalExtensionComposer
|
|
230
|
+
extension={extensionRef.current}
|
|
231
|
+
contentEditable={null}>
|
|
214
232
|
<SyncValuePlugin value={value} />
|
|
215
233
|
<RichTextPlugin
|
|
216
234
|
contentEditable={<ContentEditable />}
|
|
@@ -218,7 +236,7 @@ export function RichTextView({
|
|
|
218
236
|
ErrorBoundary={LexicalErrorBoundary}
|
|
219
237
|
/>
|
|
220
238
|
{plugins}
|
|
221
|
-
</
|
|
239
|
+
</LexicalExtensionComposer>
|
|
222
240
|
</div>
|
|
223
241
|
);
|
|
224
242
|
}
|