Skip to content

Commit 20e4ea1

Browse files
authored
[lexical] Feature: Add version identifier to LexicalEditor constructor (#6488)
1 parent e1881a6 commit 20e4ea1

File tree

18 files changed

+177
-89
lines changed

18 files changed

+177
-89
lines changed

packages/lexical-devtools-core/src/generateContent.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,8 @@ export function generateContent(
161161
} else {
162162
res += '\n └ None dispatched.';
163163
}
164-
165-
res += '\n\n editor:';
164+
const {version} = editor.constructor;
165+
res += `\n\n editor${version ? ` (v${version})` : ''}:`;
166166
res += `\n └ namespace ${editorConfig.namespace}`;
167167
if (compositionKey !== null) {
168168
res += `\n └ compositionKey ${compositionKey}`;

packages/lexical-devtools/src/utils/isLexicalNode.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
*
77
*/
88

9+
import {getEditorPropertyFromDOMNode} from 'lexical';
10+
911
import {LexicalHTMLElement} from '../types';
1012

1113
export function isLexicalNode(
1214
node: LexicalHTMLElement | Element,
1315
): node is LexicalHTMLElement {
14-
return (node as LexicalHTMLElement).__lexicalEditor !== undefined;
16+
return getEditorPropertyFromDOMNode(node) !== undefined;
1517
}

packages/lexical-playground/vite.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ export default defineConfig(({command}) => {
5252
from: /__DEV__/g,
5353
to: 'true',
5454
},
55+
{
56+
from: 'process.env.LEXICAL_VERSION',
57+
to: JSON.stringify(`${process.env.npm_package_version}+git`),
58+
},
5559
],
5660
}),
5761
babel({

packages/lexical-playground/vite.prod.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ export default defineConfig({
5353
from: /__DEV__/g,
5454
to: 'false',
5555
},
56+
{
57+
from: 'process.env.LEXICAL_VERSION',
58+
to: JSON.stringify(`${process.env.npm_package_version}+git`),
59+
},
5660
],
5761
}),
5862
babel({

packages/lexical-react/src/LexicalCheckListPlugin.tsx

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
$isElementNode,
2929
$isRangeSelection,
3030
COMMAND_PRIORITY_LOW,
31+
getNearestEditorFromDOMNode,
3132
KEY_ARROW_DOWN_COMMAND,
3233
KEY_ARROW_LEFT_COMMAND,
3334
KEY_ARROW_UP_COMMAND,
@@ -199,20 +200,20 @@ function handleCheckItemEvent(event: PointerEvent, callback: () => void) {
199200

200201
function handleClick(event: Event) {
201202
handleCheckItemEvent(event as PointerEvent, () => {
202-
const domNode = event.target as HTMLElement;
203-
const editor = findEditor(domNode);
203+
if (event.target instanceof HTMLElement) {
204+
const domNode = event.target;
205+
const editor = getNearestEditorFromDOMNode(domNode);
204206

205-
if (editor != null && editor.isEditable()) {
206-
editor.update(() => {
207-
if (event.target) {
207+
if (editor != null && editor.isEditable()) {
208+
editor.update(() => {
208209
const node = $getNearestNodeFromDOMNode(domNode);
209210

210211
if ($isListItemNode(node)) {
211212
domNode.focus();
212213
node.toggleChecked();
213214
}
214-
}
215-
});
215+
});
216+
}
216217
}
217218
});
218219
}
@@ -224,22 +225,6 @@ function handlePointerDown(event: PointerEvent) {
224225
});
225226
}
226227

227-
function findEditor(target: Node) {
228-
let node: ParentNode | Node | null = target;
229-
230-
while (node) {
231-
// @ts-ignore internal field
232-
if (node.__lexicalEditor) {
233-
// @ts-ignore internal field
234-
return node.__lexicalEditor;
235-
}
236-
237-
node = node.parentNode;
238-
}
239-
240-
return null;
241-
}
242-
243228
function getActiveCheckListItem(): HTMLElement | null {
244229
const activeElement = document.activeElement as HTMLElement;
245230

packages/lexical-website/docs/concepts/listeners.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ handle external UI state and UI features relating to specific types of node.
8484
If any existing nodes are in the DOM, and skipInitialization is not true, the listener
8585
will be called immediately with an updateTag of 'registerMutationListener' where all
8686
nodes have the 'created' NodeMutation. This can be controlled with the skipInitialization option
87-
(default is currently true for backwards compatibility in 0.16.x but will change to false in 0.17.0).
87+
(default is currently true for backwards compatibility in 0.17.x but will change to false in 0.18.0).
8888

8989
```js
9090
const removeMutationListener = editor.registerMutationListener(

packages/lexical/src/LexicalEditor.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,9 @@ export function createEditor(editorConfig?: CreateEditorArgs): LexicalEditor {
562562
export class LexicalEditor {
563563
['constructor']!: KlassConstructor<typeof LexicalEditor>;
564564

565+
/** The version with build identifiers for this editor (since 0.17.1) */
566+
static version: string | undefined;
567+
565568
/** @internal */
566569
_headless: boolean;
567570
/** @internal */
@@ -1284,3 +1287,5 @@ export class LexicalEditor {
12841287
};
12851288
}
12861289
}
1290+
1291+
LexicalEditor.version = process.env.LEXICAL_VERSION;

packages/lexical/src/LexicalEvents.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ import {
9494
getAnchorTextFromDOM,
9595
getDOMSelection,
9696
getDOMTextNode,
97+
getEditorPropertyFromDOMNode,
9798
getEditorsToPropagate,
9899
getNearestEditorFromDOMNode,
99100
getWindow,
@@ -111,6 +112,7 @@ import {
111112
isEscape,
112113
isFirefoxClipboardEvents,
113114
isItalic,
115+
isLexicalEditor,
114116
isLineBreak,
115117
isModifier,
116118
isMoveBackward,
@@ -1329,13 +1331,17 @@ export function removeRootElementEvents(rootElement: HTMLElement): void {
13291331
doc.removeEventListener('selectionchange', onDocumentSelectionChange);
13301332
}
13311333

1332-
// @ts-expect-error: internal field
1333-
const editor: LexicalEditor | null | undefined = rootElement.__lexicalEditor;
1334+
const editor = getEditorPropertyFromDOMNode(rootElement);
13341335

1335-
if (editor !== null && editor !== undefined) {
1336+
if (isLexicalEditor(editor)) {
13361337
cleanActiveNestedEditorsMap(editor);
13371338
// @ts-expect-error: internal field
13381339
rootElement.__lexicalEditor = null;
1340+
} else if (editor) {
1341+
invariant(
1342+
false,
1343+
'Attempted to remove event handlers from a node that does not belong to this build of Lexical',
1344+
);
13391345
}
13401346

13411347
const removeHandles = getRootElementRemoveHandles(rootElement);

packages/lexical/src/LexicalUpdates.ts

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,24 @@
66
*
77
*/
88

9-
import type {
9+
import type {SerializedEditorState} from './LexicalEditorState';
10+
import type {LexicalNode, SerializedLexicalNode} from './LexicalNode';
11+
12+
import invariant from 'shared/invariant';
13+
14+
import {$isElementNode, $isTextNode, SELECTION_CHANGE_COMMAND} from '.';
15+
import {FULL_RECONCILE, NO_DIRTY_NODES} from './LexicalConstants';
16+
import {
1017
CommandPayloadType,
1118
EditorUpdateOptions,
1219
LexicalCommand,
1320
LexicalEditor,
1421
Listener,
1522
MutatedNodes,
1623
RegisteredNodes,
24+
resetEditor,
1725
Transform,
1826
} from './LexicalEditor';
19-
import type {SerializedEditorState} from './LexicalEditorState';
20-
import type {LexicalNode, SerializedLexicalNode} from './LexicalNode';
21-
22-
import invariant from 'shared/invariant';
23-
24-
import {$isElementNode, $isTextNode, SELECTION_CHANGE_COMMAND} from '.';
25-
import {FULL_RECONCILE, NO_DIRTY_NODES} from './LexicalConstants';
26-
import {resetEditor} from './LexicalEditor';
2727
import {
2828
cloneEditorState,
2929
createEmptyEditorState,
@@ -47,9 +47,11 @@ import {
4747
import {
4848
$getCompositionKey,
4949
getDOMSelection,
50+
getEditorPropertyFromDOMNode,
5051
getEditorStateTextContent,
5152
getEditorsToPropagate,
5253
getRegisteredNodeOrThrow,
54+
isLexicalEditor,
5355
removeDOMBlockCursorElement,
5456
scheduleMicroTask,
5557
updateDOMBlockCursorElement,
@@ -96,7 +98,8 @@ export function getActiveEditorState(): EditorState {
9698
'Unable to find an active editor state. ' +
9799
'State helpers or node methods can only be used ' +
98100
'synchronously during the callback of ' +
99-
'editor.update(), editor.read(), or editorState.read().',
101+
'editor.update(), editor.read(), or editorState.read().%s',
102+
collectBuildInformation(),
100103
);
101104
}
102105

@@ -110,13 +113,46 @@ export function getActiveEditor(): LexicalEditor {
110113
'Unable to find an active editor. ' +
111114
'This method can only be used ' +
112115
'synchronously during the callback of ' +
113-
'editor.update() or editor.read().',
116+
'editor.update() or editor.read().%s',
117+
collectBuildInformation(),
114118
);
115119
}
116-
117120
return activeEditor;
118121
}
119122

123+
function collectBuildInformation(): string {
124+
let compatibleEditors = 0;
125+
const incompatibleEditors = new Set<string>();
126+
const thisVersion = LexicalEditor.version;
127+
if (typeof window !== 'undefined') {
128+
for (const node of document.querySelectorAll('[contenteditable]')) {
129+
const editor = getEditorPropertyFromDOMNode(node);
130+
if (isLexicalEditor(editor)) {
131+
compatibleEditors++;
132+
} else if (editor) {
133+
let version = String(
134+
(
135+
editor.constructor as typeof editor['constructor'] &
136+
Record<string, unknown>
137+
).version || '<0.17.1',
138+
);
139+
if (version === thisVersion) {
140+
version +=
141+
' (separately built, likely a bundler configuration issue)';
142+
}
143+
incompatibleEditors.add(version);
144+
}
145+
}
146+
}
147+
let output = ` Detected on the page: ${compatibleEditors} compatible editor(s) with version ${thisVersion}`;
148+
if (incompatibleEditors.size) {
149+
output += ` and incompatible editors with versions ${Array.from(
150+
incompatibleEditors,
151+
).join(', ')}`;
152+
}
153+
return output;
154+
}
155+
120156
export function internalGetActiveEditor(): LexicalEditor | null {
121157
return activeEditor;
122158
}

packages/lexical/src/LexicalUtils.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,7 @@ export function isSelectionCapturedInDecoratorInput(anchorDOM: Node): boolean {
123123
(nodeName === 'INPUT' ||
124124
nodeName === 'TEXTAREA' ||
125125
(activeElement.contentEditable === 'true' &&
126-
// @ts-ignore internal field
127-
activeElement.__lexicalEditor == null))
126+
getEditorPropertyFromDOMNode(activeElement) == null))
128127
);
129128
}
130129

@@ -149,21 +148,34 @@ export function isSelectionWithinEditor(
149148
}
150149
}
151150

151+
/**
152+
* @returns true if the given argument is a LexicalEditor instance from this build of Lexical
153+
*/
154+
export function isLexicalEditor(editor: unknown): editor is LexicalEditor {
155+
// Check instanceof to prevent issues with multiple embedded Lexical installations
156+
return editor instanceof LexicalEditor;
157+
}
158+
152159
export function getNearestEditorFromDOMNode(
153160
node: Node | null,
154161
): LexicalEditor | null {
155162
let currentNode = node;
156163
while (currentNode != null) {
157-
// @ts-expect-error: internal field
158-
const editor: LexicalEditor = currentNode.__lexicalEditor;
159-
if (editor != null) {
164+
const editor = getEditorPropertyFromDOMNode(currentNode);
165+
if (isLexicalEditor(editor)) {
160166
return editor;
161167
}
162168
currentNode = getParentElement(currentNode);
163169
}
164170
return null;
165171
}
166172

173+
/** @internal */
174+
export function getEditorPropertyFromDOMNode(node: Node | null): unknown {
175+
// @ts-expect-error: internal field
176+
return node ? node.__lexicalEditor : null;
177+
}
178+
167179
export function getTextDirection(text: string): 'ltr' | 'rtl' | null {
168180
if (RTL_REGEX.test(text)) {
169181
return 'rtl';

packages/lexical/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,13 @@ export {
176176
$setCompositionKey,
177177
$setSelection,
178178
$splitNode,
179+
getEditorPropertyFromDOMNode,
179180
getNearestEditorFromDOMNode,
180181
isBlockDomNode,
181182
isHTMLAnchorElement,
182183
isHTMLElement,
183184
isInlineDomNode,
185+
isLexicalEditor,
184186
isSelectionCapturedInDecoratorInput,
185187
isSelectionWithinEditor,
186188
resetRandomKey,
Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,30 @@
11
{
22
"name": "lexical-esm-astro-react",
33
"type": "module",
4-
"version": "0.0.1",
4+
"version": "0.17.0",
55
"scripts": {
66
"dev": "astro dev",
77
"start": "astro dev",
88
"build": "astro check && astro build",
99
"preview": "astro preview",
10-
"astro": "astro",
10+
"astro": "astro",
1111
"test": "playwright test"
1212
},
1313
"dependencies": {
1414
"@astrojs/check": "^0.5.9",
1515
"@astrojs/react": "^3.1.0",
16-
"@lexical/react": "^0.14.3",
17-
"@lexical/utils": "^0.14.3",
16+
"@lexical/react": "0.17.0",
17+
"@lexical/utils": "0.17.0",
1818
"@types/react": "^18.2.66",
1919
"@types/react-dom": "^18.2.22",
2020
"astro": "^4.5.4",
21-
"lexical": "^0.14.3",
21+
"lexical": "0.17.0",
2222
"react": "^18.2.0",
2323
"react-dom": "^18.2.0",
2424
"typescript": "^5.4.2"
2525
},
2626
"devDependencies": {
2727
"@playwright/test": "^1.43.1"
28-
}
28+
},
29+
"sideEffects": false
2930
}

scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "lexical-esm-nextjs",
3-
"version": "0.1.0",
3+
"version": "0.17.0",
44
"private": true,
55
"scripts": {
66
"dev": "next dev",
@@ -9,9 +9,9 @@
99
"test": "playwright test"
1010
},
1111
"dependencies": {
12-
"@lexical/plain-text": "^0.14.5",
13-
"@lexical/react": "^0.14.5",
14-
"lexical": "^0.14.5",
12+
"@lexical/plain-text": "0.17.0",
13+
"@lexical/react": "0.17.0",
14+
"lexical": "0.17.0",
1515
"next": "^14.2.1",
1616
"react": "^18",
1717
"react-dom": "^18"

0 commit comments

Comments
 (0)