Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[lexical] Feature: Add version identifier to LexicalEditor constructor #6488

Merged
merged 9 commits into from
Aug 6, 2024
4 changes: 2 additions & 2 deletions packages/lexical-devtools-core/src/generateContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ export function generateContent(
} else {
res += '\n └ None dispatched.';
}

res += '\n\n editor:';
const {version} = editor.constructor;
res += `\n\n editor${version ? ` (v${version})` : ''}:`;
res += `\n └ namespace ${editorConfig.namespace}`;
if (compositionKey !== null) {
res += `\n └ compositionKey ${compositionKey}`;
Expand Down
4 changes: 3 additions & 1 deletion packages/lexical-devtools/src/utils/isLexicalNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
*
*/

import {getEditorPropertyFromDOMNode} from 'lexical';

import {LexicalHTMLElement} from '../types';

export function isLexicalNode(
node: LexicalHTMLElement | Element,
): node is LexicalHTMLElement {
return (node as LexicalHTMLElement).__lexicalEditor !== undefined;
return getEditorPropertyFromDOMNode(node) !== undefined;
}
4 changes: 4 additions & 0 deletions packages/lexical-playground/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export default defineConfig(({command}) => {
from: /__DEV__/g,
to: 'true',
},
{
from: 'process.env.LEXICAL_VERSION',
to: JSON.stringify(`${process.env.npm_package_version}+git`),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The playground is never built with an npm release of lexical since it directly includes all of the source

},
],
}),
babel({
Expand Down
4 changes: 4 additions & 0 deletions packages/lexical-playground/vite.prod.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ export default defineConfig({
from: /__DEV__/g,
to: 'false',
},
{
from: 'process.env.LEXICAL_VERSION',
to: JSON.stringify(`${process.env.npm_package_version}+git`),
},
],
}),
babel({
Expand Down
31 changes: 8 additions & 23 deletions packages/lexical-react/src/LexicalCheckListPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
$isElementNode,
$isRangeSelection,
COMMAND_PRIORITY_LOW,
getNearestEditorFromDOMNode,
KEY_ARROW_DOWN_COMMAND,
KEY_ARROW_LEFT_COMMAND,
KEY_ARROW_UP_COMMAND,
Expand Down Expand Up @@ -199,20 +200,20 @@ function handleCheckItemEvent(event: PointerEvent, callback: () => void) {

function handleClick(event: Event) {
handleCheckItemEvent(event as PointerEvent, () => {
const domNode = event.target as HTMLElement;
const editor = findEditor(domNode);
if (event.target instanceof HTMLElement) {
const domNode = event.target;
const editor = getNearestEditorFromDOMNode(domNode);

if (editor != null && editor.isEditable()) {
editor.update(() => {
if (event.target) {
if (editor != null && editor.isEditable()) {
editor.update(() => {
const node = $getNearestNodeFromDOMNode(domNode);

if ($isListItemNode(node)) {
domNode.focus();
node.toggleChecked();
}
}
});
});
}
}
});
}
Expand All @@ -224,22 +225,6 @@ function handlePointerDown(event: PointerEvent) {
});
}

function findEditor(target: Node) {
let node: ParentNode | Node | null = target;

while (node) {
// @ts-ignore internal field
if (node.__lexicalEditor) {
// @ts-ignore internal field
return node.__lexicalEditor;
}

node = node.parentNode;
}

return null;
}

function getActiveCheckListItem(): HTMLElement | null {
const activeElement = document.activeElement as HTMLElement;

Expand Down
2 changes: 1 addition & 1 deletion packages/lexical-website/docs/concepts/listeners.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ handle external UI state and UI features relating to specific types of node.
If any existing nodes are in the DOM, and skipInitialization is not true, the listener
will be called immediately with an updateTag of 'registerMutationListener' where all
nodes have the 'created' NodeMutation. This can be controlled with the skipInitialization option
(default is currently true for backwards compatibility in 0.16.x but will change to false in 0.17.0).
(default is currently true for backwards compatibility in 0.17.x but will change to false in 0.18.0).
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed the deprecation policy here since 0.17.0 was the first release with the changes to registerMutationListener (#6357)


```js
const removeMutationListener = editor.registerMutationListener(
Expand Down
5 changes: 5 additions & 0 deletions packages/lexical/src/LexicalEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,9 @@ export function createEditor(editorConfig?: CreateEditorArgs): LexicalEditor {
export class LexicalEditor {
['constructor']!: KlassConstructor<typeof LexicalEditor>;

/** The version with build identifiers for this editor (since 0.17.1) */
static version: string | undefined;

/** @internal */
_headless: boolean;
/** @internal */
Expand Down Expand Up @@ -1284,3 +1287,5 @@ export class LexicalEditor {
};
}
}

LexicalEditor.version = process.env.LEXICAL_VERSION;
12 changes: 9 additions & 3 deletions packages/lexical/src/LexicalEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ import {
getAnchorTextFromDOM,
getDOMSelection,
getDOMTextNode,
getEditorPropertyFromDOMNode,
getEditorsToPropagate,
getNearestEditorFromDOMNode,
getWindow,
Expand All @@ -111,6 +112,7 @@ import {
isEscape,
isFirefoxClipboardEvents,
isItalic,
isLexicalEditor,
isLineBreak,
isModifier,
isMoveBackward,
Expand Down Expand Up @@ -1329,13 +1331,17 @@ export function removeRootElementEvents(rootElement: HTMLElement): void {
doc.removeEventListener('selectionchange', onDocumentSelectionChange);
}

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

if (editor !== null && editor !== undefined) {
if (isLexicalEditor(editor)) {
cleanActiveNestedEditorsMap(editor);
// @ts-expect-error: internal field
rootElement.__lexicalEditor = null;
} else if (editor) {
invariant(
false,
'Attempted to remove event handlers from a node that does not belong to this build of Lexical',
);
}

const removeHandles = getRootElementRemoveHandles(rootElement);
Expand Down
60 changes: 48 additions & 12 deletions packages/lexical/src/LexicalUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,24 @@
*
*/

import type {
import type {SerializedEditorState} from './LexicalEditorState';
import type {LexicalNode, SerializedLexicalNode} from './LexicalNode';

import invariant from 'shared/invariant';

import {$isElementNode, $isTextNode, SELECTION_CHANGE_COMMAND} from '.';
import {FULL_RECONCILE, NO_DIRTY_NODES} from './LexicalConstants';
import {
CommandPayloadType,
EditorUpdateOptions,
LexicalCommand,
LexicalEditor,
Listener,
MutatedNodes,
RegisteredNodes,
resetEditor,
Transform,
} from './LexicalEditor';
import type {SerializedEditorState} from './LexicalEditorState';
import type {LexicalNode, SerializedLexicalNode} from './LexicalNode';

import invariant from 'shared/invariant';

import {$isElementNode, $isTextNode, SELECTION_CHANGE_COMMAND} from '.';
import {FULL_RECONCILE, NO_DIRTY_NODES} from './LexicalConstants';
import {resetEditor} from './LexicalEditor';
import {
cloneEditorState,
createEmptyEditorState,
Expand All @@ -47,9 +47,11 @@ import {
import {
$getCompositionKey,
getDOMSelection,
getEditorPropertyFromDOMNode,
getEditorStateTextContent,
getEditorsToPropagate,
getRegisteredNodeOrThrow,
isLexicalEditor,
removeDOMBlockCursorElement,
scheduleMicroTask,
updateDOMBlockCursorElement,
Expand Down Expand Up @@ -96,7 +98,8 @@ export function getActiveEditorState(): EditorState {
'Unable to find an active editor state. ' +
'State helpers or node methods can only be used ' +
'synchronously during the callback of ' +
'editor.update(), editor.read(), or editorState.read().',
'editor.update(), editor.read(), or editorState.read().%s',
collectBuildInformation(),
);
}

Expand All @@ -110,13 +113,46 @@ export function getActiveEditor(): LexicalEditor {
'Unable to find an active editor. ' +
'This method can only be used ' +
'synchronously during the callback of ' +
'editor.update() or editor.read().',
'editor.update() or editor.read().%s',
collectBuildInformation(),
);
}

return activeEditor;
}

function collectBuildInformation(): string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@etrepum nitpicking but such detailed error is something I'd consider gating under the __DEV__ build or developer extension. I don't think it's very common to have multiple editors on the same page and it's less common to make this mistake, and there is quite some logic in here 679 bytes minimized.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the amount of bug reports for this one issue I was motivated to include it, I think a lot of people end up with the prod build for various reasons. You could put it behind a flag so it doesn't get included in www? Once dev tools gets a release with this functionality we could stub the implementation out with a string that says to install the dev tools to diagnose. We are still talking about <1kb before compression.

let compatibleEditors = 0;
const incompatibleEditors = new Set<string>();
const thisVersion = LexicalEditor.version;
if (typeof window !== 'undefined') {
for (const node of document.querySelectorAll('[contenteditable]')) {
const editor = getEditorPropertyFromDOMNode(node);
if (isLexicalEditor(editor)) {
compatibleEditors++;
} else if (editor) {
let version = String(
(
editor.constructor as typeof editor['constructor'] &
Record<string, unknown>
).version || '<0.17.1',
);
if (version === thisVersion) {
version +=
' (separately built, likely a bundler configuration issue)';
}
incompatibleEditors.add(version);
}
}
}
let output = ` Detected on the page: ${compatibleEditors} compatible editor(s) with version ${thisVersion}`;
if (incompatibleEditors.size) {
output += ` and incompatible editors with versions ${Array.from(
incompatibleEditors,
).join(', ')}`;
}
return output;
}

export function internalGetActiveEditor(): LexicalEditor | null {
return activeEditor;
}
Expand Down
22 changes: 17 additions & 5 deletions packages/lexical/src/LexicalUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,7 @@ export function isSelectionCapturedInDecoratorInput(anchorDOM: Node): boolean {
(nodeName === 'INPUT' ||
nodeName === 'TEXTAREA' ||
(activeElement.contentEditable === 'true' &&
// @ts-ignore internal field
activeElement.__lexicalEditor == null))
getEditorPropertyFromDOMNode(activeElement) == null))
);
}

Expand All @@ -149,21 +148,34 @@ export function isSelectionWithinEditor(
}
}

/**
* @returns true if the given argument is a LexicalEditor instance from this build of Lexical
*/
export function isLexicalEditor(editor: unknown): editor is LexicalEditor {
// Check instanceof to prevent issues with multiple embedded Lexical installations
return editor instanceof LexicalEditor;
}

export function getNearestEditorFromDOMNode(
node: Node | null,
): LexicalEditor | null {
let currentNode = node;
while (currentNode != null) {
// @ts-expect-error: internal field
const editor: LexicalEditor = currentNode.__lexicalEditor;
if (editor != null) {
const editor = getEditorPropertyFromDOMNode(currentNode);
if (isLexicalEditor(editor)) {
return editor;
}
currentNode = getParentElement(currentNode);
}
return null;
}

/** @internal */
export function getEditorPropertyFromDOMNode(node: Node | null): unknown {
// @ts-expect-error: internal field
return node ? node.__lexicalEditor : null;
}

export function getTextDirection(text: string): 'ltr' | 'rtl' | null {
if (RTL_REGEX.test(text)) {
return 'rtl';
Expand Down
2 changes: 2 additions & 0 deletions packages/lexical/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,13 @@ export {
$setCompositionKey,
$setSelection,
$splitNode,
getEditorPropertyFromDOMNode,
getNearestEditorFromDOMNode,
isBlockDomNode,
isHTMLAnchorElement,
isHTMLElement,
isInlineDomNode,
isLexicalEditor,
isSelectionCapturedInDecoratorInput,
isSelectionWithinEditor,
resetRandomKey,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
{
"name": "lexical-esm-astro-react",
"type": "module",
"version": "0.0.1",
"version": "0.17.0",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro",
"astro": "astro",
"test": "playwright test"
},
"dependencies": {
"@astrojs/check": "^0.5.9",
"@astrojs/react": "^3.1.0",
"@lexical/react": "^0.14.3",
"@lexical/utils": "^0.14.3",
"@lexical/react": "0.17.0",
"@lexical/utils": "0.17.0",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"astro": "^4.5.4",
"lexical": "^0.14.3",
"lexical": "0.17.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^5.4.2"
},
"devDependencies": {
"@playwright/test": "^1.43.1"
}
},
"sideEffects": false
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lexical-esm-nextjs",
"version": "0.1.0",
"version": "0.17.0",
"private": true,
"scripts": {
"dev": "next dev",
Expand All @@ -9,9 +9,9 @@
"test": "playwright test"
},
"dependencies": {
"@lexical/plain-text": "^0.14.5",
"@lexical/react": "^0.14.5",
"lexical": "^0.14.5",
"@lexical/plain-text": "0.17.0",
"@lexical/react": "0.17.0",
"lexical": "0.17.0",
"next": "^14.2.1",
"react": "^18",
"react-dom": "^18"
Expand Down
Loading
Loading