Add unstable serialization logic for node JSON parsing (#2157)

* Add unstable serialization logic for node JSON parsing

* Ooops

* Freeze editorState

* Migrate code node

* Address feedback

* Address feedback

* Address feedback

* Address more feedback

* Address more feedback

* Address FlowFixMes

* update types

* prettier

* remove import

* polish types

* fix types

* add ut for unstable APIs

* fix rebase issue

* oops

* wip

* more nodes

* types

* prettier

* add tests for core nodes

* update codes.json

* Merge global files

* Rename global type defs

* Update packages/lexical-link/src/index.js

Co-authored-by: Gerard Rovira <zurfyx@users.noreply.github.com>

* fix linter an versions

* more versions

Co-authored-by: acywatson <acy.watson@gmail.com>
Co-authored-by: John Flockton <thegreatercurve@users.noreply.github.com>
Co-authored-by: Gerard Rovira <zurfyx@users.noreply.github.com>
This commit is contained in:
Dominic Gannaway
2022-05-19 19:44:52 +01:00
committed by GitHub
parent 8d54925925
commit 8f9a903785
55 changed files with 1526 additions and 43 deletions

View File

@ -10,3 +10,5 @@ declare module '*.jpg' {
const content: any;
export default content;
}
export type Spread<T1, T2> = {[K in Exclude<keyof T1, keyof T2>]: T1[K]} & T2;

View File

@ -14,9 +14,12 @@ import type {
RangeSelection,
EditorThemeClasses,
LexicalEditor,
SerializedElementNode,
SerializedTextNode,
} from 'lexical';
import {ElementNode, TextNode} from 'lexical';
import {Spread} from 'libdefs/globals';
declare class CodeNode extends ElementNode {
static getType(): string;
@ -31,6 +34,8 @@ declare class CodeNode extends ElementNode {
collapseAtStart(): true;
setLanguage(language: string): void;
getLanguage(): string | void;
importJSON(serializedNode: SerializedCodeNode): CodeNode;
exportJSON(): SerializedElementNode;
}
declare function $createCodeNode(language?: string): CodeNode;
declare function $isCodeNode(
@ -74,3 +79,21 @@ declare function $isCodeHighlightNode(
): node is CodeHighlightNode;
declare function registerCodeHighlighting(editor: LexicalEditor): () => void;
type SerializedCodeNode = Spread<
{
language: string | null | undefined;
type: 'code';
version: 1;
},
SerializedElementNode
>;
type SerializedCodeHighlightNode = Spread<
{
highlightType: string | null | undefined;
type: 'code-highlight';
version: 1;
},
SerializedTextNode
>;

View File

@ -18,6 +18,8 @@ import type {
NodeKey,
ParagraphNode,
RangeSelection,
SerializedElementNode,
SerializedTextNode,
} from 'lexical';
import * as Prism from 'prismjs';
@ -39,6 +41,7 @@ import {
mergeRegister,
removeClassNamesFromElement,
} from '@lexical/utils';
import {Spread} from 'globals';
import {
$createLineBreakNode,
$createParagraphNode,
@ -59,6 +62,24 @@ import {
const DEFAULT_CODE_LANGUAGE = 'javascript';
type SerializedCodeNode = Spread<
{
language: string | null | undefined;
type: 'code';
version: 1;
},
SerializedElementNode
>;
type SerializedCodeHighlightNode = Spread<
{
highlightType: string | null | undefined;
type: 'code-highlight';
version: 1;
},
SerializedTextNode
>;
const mapToPrismLanguage = (
language: string | null | undefined,
): string | null | undefined => {
@ -99,6 +120,11 @@ export class CodeHighlightNode extends TextNode {
);
}
getHighlightType(): string | null | undefined {
const self = this.getLatest<CodeHighlightNode>();
return self.__highlightType;
}
createDOM(config: EditorConfig): HTMLElement {
const element = super.createDOM(config);
const className = getHighlightThemeClass(
@ -134,6 +160,25 @@ export class CodeHighlightNode extends TextNode {
return update;
}
static importJSON(
serializedNode: SerializedCodeHighlightNode,
): CodeHighlightNode {
const node = $createCodeHighlightNode(serializedNode.highlightType);
node.setFormat(serializedNode.format);
node.setDetail(serializedNode.detail);
node.setMode(serializedNode.mode);
node.setStyle(serializedNode.style);
return node;
}
exportJSON(): SerializedCodeHighlightNode {
return {
...super.exportJSON(),
highlightType: this.getHighlightType(),
type: 'code-highlight',
};
}
// Prevent formatting (bold, underline, etc)
setFormat(format: number): this {
return this;
@ -266,6 +311,22 @@ export class CodeNode extends ElementNode {
};
}
static importJSON(serializedNode: SerializedCodeNode): CodeNode {
const node = $createCodeNode(serializedNode.language);
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction);
return node;
}
exportJSON(): SerializedCodeNode {
return {
...super.exportJSON(),
language: this.getLanguage(),
type: 'code',
};
}
// Mutation
insertNewAfter(
selection: RangeSelection,

View File

@ -6,7 +6,12 @@
*
*/
import type {EditorConfig, LexicalNode, NodeKey} from 'lexical';
import type {
EditorConfig,
LexicalNode,
NodeKey,
SerializedTextNode,
} from 'lexical';
import {TextNode} from 'lexical';
export declare class HashtagNode extends TextNode {
@ -16,6 +21,7 @@ export declare class HashtagNode extends TextNode {
createDOM(config: EditorConfig): HTMLElement;
canInsertTextBefore(): boolean;
isTextEntity(): true;
static importJSON(serializedNode: SerializedTextNode): HashtagNode;
}
export function $createHashtagNode(text?: string): TextNode;
export function $isHashtagNode(

View File

@ -7,17 +7,24 @@
* @flow strict
*/
import type {EditorConfig, LexicalNode, NodeKey} from 'lexical';
import type {
EditorConfig,
LexicalNode,
NodeKey,
SerializedTextNode,
} from 'lexical';
import {TextNode} from 'lexical';
declare export class HashtagNode extends TextNode {
static getType(): string;
static clone(node: HashtagNode): HashtagNode;
static importJSON(serializedNode: SerializedTextNode): HashtagNode;
constructor(text: string, key?: NodeKey): void;
createDOM(config: EditorConfig): HTMLElement;
canInsertTextBefore(): boolean;
isTextEntity(): true;
exportJSON(): SerializedTextNode;
}
declare export function $createHashtagNode(text?: string): HashtagNode;
declare export function $isHashtagNode(

View File

@ -7,7 +7,12 @@
* @flow strict
*/
import type {EditorConfig, LexicalNode, NodeKey} from 'lexical';
import type {
EditorConfig,
LexicalNode,
NodeKey,
SerializedTextNode,
} from 'lexical';
import {addClassNamesToElement} from '@lexical/utils';
import {TextNode} from 'lexical';
@ -31,6 +36,22 @@ export class HashtagNode extends TextNode {
return element;
}
static importJSON(serializedNode: SerializedTextNode): HashtagNode {
const node = $createHashtagNode(serializedNode.text);
node.setFormat(serializedNode.format);
node.setDetail(serializedNode.detail);
node.setMode(serializedNode.mode);
node.setStyle(serializedNode.style);
return node;
}
exportJSON(): SerializedTextNode {
return {
...super.exportJSON(),
type: 'hashtag',
};
}
canInsertTextBefore(): boolean {
return false;
}

View File

@ -15,10 +15,20 @@ import type {
LexicalNode,
NodeKey,
RangeSelection,
SerializedElementNode,
} from 'lexical';
import {addClassNamesToElement} from '@lexical/utils';
import {$isElementNode, createCommand, ElementNode} from 'lexical';
import invariant from 'shared/invariant';
export type SerializedLinkNode = {
...SerializedElementNode,
type: 'link',
url: string,
version: 1,
...
};
export class LinkNode extends ElementNode {
__url: string;
@ -67,6 +77,22 @@ export class LinkNode extends ElementNode {
};
}
static importJSON(serializedNode: SerializedLinkNode): LinkNode {
const node = $createLinkNode(serializedNode.url);
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction);
return node;
}
exportJSON(): SerializedElementNode {
return {
...super.exportJSON(),
type: 'link',
url: this.getURL(),
};
}
getURL(): string {
return this.getLatest().__url;
}
@ -119,6 +145,13 @@ export function $isLinkNode(node: ?LexicalNode): boolean %checks {
return node instanceof LinkNode;
}
export type SerializedAutoLinkNode = {
...SerializedLinkNode,
type: 'autolink',
version: 1,
...
};
// Custom node type to override `canInsertTextAfter` that will
// allow typing within the link
export class AutoLinkNode extends LinkNode {
@ -131,6 +164,28 @@ export class AutoLinkNode extends LinkNode {
return new AutoLinkNode(node.__url, node.__key);
}
static importJSON(
serializedNode: SerializedLinkNode | SerializedAutoLinkNode,
): AutoLinkNode {
invariant(
serializedNode.type !== 'autolink',
'Incorrect node type received in importJSON for %s',
this.getType(),
);
const node = $createAutoLinkNode(serializedNode.url);
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction);
return node;
}
exportJSON(): SerializedElementNode {
return {
...super.exportJSON(),
type: 'autolink',
};
}
insertNewAfter(selection: RangeSelection): null | ElementNode {
const element = this.getParentOrThrow().insertNewAfter(selection);
if ($isElementNode(element)) {

View File

@ -7,6 +7,7 @@
*/
import {ListNodeTagType} from './src/LexicalListNode';
import {Spread} from 'globals';
import {
ElementNode,
LexicalNode,
@ -14,6 +15,7 @@ import {
ParagraphNode,
RangeSelection,
LexicalCommand,
SerializedElementNode,
} from 'lexical';
export type ListType = 'number' | 'bullet' | 'check';
@ -40,13 +42,18 @@ export declare class ListItemNode extends ElementNode {
getChecked(): boolean | void;
setChecked(boolean): this;
toggleChecked(): void;
static importJSON(serializedNode: SerializedListItemNode): ListItemNode;
exportJSON(): SerializedListItemNode;
}
export declare class ListNode extends ElementNode {
canBeEmpty(): false;
append(...nodesToAppend: LexicalNode[]): ListNode;
getTag(): ListNodeTagType;
getListType(): ListType;
static importJSON(serializedNode: SerializedListNode): ListNode;
exportJSON(): SerializedListNode;
}
export function outdentList(): void;
export function removeList(editor: LexicalEditor): boolean;
@ -54,3 +61,22 @@ export var INSERT_UNORDERED_LIST_COMMAND: LexicalCommand<void>;
export var INSERT_ORDERED_LIST_COMMAND: LexicalCommand<void>;
export var INSERT_CHECK_LIST_COMMAND: LexicalCommand<void>;
export var REMOVE_LIST_COMMAND: LexicalCommand<void>;
export type SerializedListItemNode = Spread<
{
checked: boolean | void;
value: number;
type: 'listitem';
},
SerializedElementNode
>;
export type SerializedListNode = Spread<
{
listType: ListType;
start: number;
tag: ListNodeTagType;
type: 'list';
},
SerializedElementNode
>;

View File

@ -13,6 +13,7 @@ import type {
ParagraphNode,
RangeSelection,
LexicalCommand,
SerializedElementNode,
} from 'lexical';
import {ElementNode} from 'lexical';
@ -53,6 +54,7 @@ declare export class ListItemNode extends ElementNode {
getChecked(): boolean | void;
setChecked(boolean): this;
toggleChecked(): void;
static importJSON(serializedNode: SerializedListItemNode): ListItemNode;
}
declare export class ListNode extends ElementNode {
__tag: ListNodeTagType;
@ -62,6 +64,7 @@ declare export class ListNode extends ElementNode {
getTag(): ListNodeTagType;
getStart(): number;
getListType(): ListType;
static importJSON(serializedNode: SerializedListNode): ListNode;
}
declare export function outdentList(): void;
declare export function removeList(editor: LexicalEditor): boolean;
@ -70,3 +73,22 @@ declare export var INSERT_UNORDERED_LIST_COMMAND: LexicalCommand<void>;
declare export var INSERT_ORDERED_LIST_COMMAND: LexicalCommand<void>;
declare export var INSERT_CHECK_LIST_COMMAND: LexicalCommand<void>;
declare export var REMOVE_LIST_COMMAND: LexicalCommand<void>;
export type SerializedListItemNode = {
...SerializedElementNode,
checked: boolean | void,
value: number,
type: 'listitem',
version: 1,
...
};
export type SerializedListNode = {
...SerializedElementNode,
listType: ListType,
start: number,
tag: ListNodeTagType,
type: 'list',
version: 1,
...
};

View File

@ -19,6 +19,7 @@ import type {
NodeSelection,
ParagraphNode,
RangeSelection,
SerializedElementNode,
} from 'lexical';
import {
@ -41,6 +42,15 @@ import {
updateChildrenListItemValue,
} from './formatList';
export type SerializedListItemNode = {
...SerializedElementNode,
checked: boolean | void,
type: 'listitem',
value: number,
version: 1,
...
};
export class ListItemNode extends ElementNode {
__value: number;
__checked: boolean | void;
@ -96,6 +106,23 @@ export class ListItemNode extends ElementNode {
};
}
static importJSON(serializedNode: SerializedListItemNode): ListItemNode {
const node = new ListItemNode(serializedNode.value, serializedNode.checked);
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction);
return node;
}
exportJSON(): SerializedElementNode {
return {
...super.exportJSON(),
checked: this.getChecked(),
type: 'listitem',
value: this.getValue(),
};
}
append(...nodes: LexicalNode[]): ListItemNode {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
@ -262,8 +289,13 @@ export class ListItemNode extends ElementNode {
}
getIndent(): number {
// If we don't have a parent, we are likely serializing
const parent = this.getParent();
if (parent === null) {
return this.getLatest().__indent;
}
// ListItemNode should always have a ListNode for a parent.
let listNodeParent = this.getParentOrThrow().getParentOrThrow();
let listNodeParent = parent.getParentOrThrow();
let indentLevel = 0;
while ($isListItemNode(listNodeParent)) {
listNodeParent = listNodeParent.getParentOrThrow().getParentOrThrow();

View File

@ -14,6 +14,7 @@ import type {
EditorThemeClasses,
LexicalNode,
NodeKey,
SerializedElementNode,
} from 'lexical';
import {
@ -25,6 +26,16 @@ import {$createTextNode, ElementNode} from 'lexical';
import {$createListItemNode, $isListItemNode} from '.';
import {$getListDepth} from './utils';
export type SerializedListNode = {
...SerializedElementNode,
listType: ListType,
start: number,
tag: ListNodeTagType,
type: 'list',
version: 1,
...
};
export type ListType = 'number' | 'bullet' | 'check';
export type ListNodeTagType = 'ul' | 'ol';
@ -103,6 +114,24 @@ export class ListNode extends ElementNode {
};
}
static importJSON(serializedNode: SerializedListNode): ListNode {
const node = $createListNode(serializedNode.listType, serializedNode.start);
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction);
return node;
}
exportJSON(): SerializedElementNode {
return {
...super.exportJSON(),
listType: this.getListType(),
start: this.getStart(),
tag: this.getTag(),
type: 'list',
};
}
canBeEmpty(): false {
return false;
}

View File

@ -6,7 +6,14 @@
*
* @flow strict
*/
import type {EditorConfig, LexicalNode, NodeKey, RangeSelection} from 'lexical';
import type {
EditorConfig,
LexicalNode,
NodeKey,
RangeSelection,
SerializedElementNode,
} from 'lexical';
import {Spread} from 'globals';
import {ElementNode} from 'lexical';
export declare class OverflowNode extends ElementNode {
static getType(): string;
@ -16,6 +23,16 @@ export declare class OverflowNode extends ElementNode {
updateDOM(prevNode: OverflowNode, dom: HTMLElement): boolean;
insertNewAfter(selection: RangeSelection): null | LexicalNode;
excludeFromCopy(): boolean;
static importJSON(serializedNode: SerializedOverflowNode): OverflowNode;
exportJSON(): SerializedElementNode;
}
export function $createOverflowNode(): OverflowNode;
export function $isOverflowNode(node: LexicalNode | null): node is OverflowNode;
export type SerializedOverflowNode = Spread<
{
type: 'overflow';
},
SerializedElementNode
>;

View File

@ -6,7 +6,13 @@
*
* @flow strict
*/
import type {EditorConfig, LexicalNode, NodeKey, RangeSelection} from 'lexical';
import type {
EditorConfig,
LexicalNode,
NodeKey,
RangeSelection,
SerializedElementNode,
} from 'lexical';
import {ElementNode} from 'lexical';
declare export class OverflowNode extends ElementNode {
static getType(): string;
@ -16,8 +22,16 @@ declare export class OverflowNode extends ElementNode {
updateDOM(prevNode: OverflowNode, dom: HTMLElement): boolean;
insertNewAfter(selection: RangeSelection): null | LexicalNode;
excludeFromCopy(): boolean;
static importJSON(serializedNode: SerializedOverflowNode): OverflowNode;
}
declare export function $createOverflowNode(): OverflowNode;
declare export function $isOverflowNode(
node: ?LexicalNode,
): boolean %checks(node instanceof OverflowNode);
export type SerializedOverflowNode = {
...SerializedElementNode,
type: 'overflow',
version: 1,
...
};

View File

@ -7,10 +7,23 @@
* @flow strict
*/
import type {EditorConfig, LexicalNode, NodeKey, RangeSelection} from 'lexical';
import type {
EditorConfig,
LexicalNode,
NodeKey,
RangeSelection,
SerializedElementNode,
} from 'lexical';
import {ElementNode} from 'lexical';
export type SerializedOverflowNode = {
...SerializedElementNode,
type: 'overflow',
version: 1,
...
};
export class OverflowNode extends ElementNode {
static getType(): string {
return 'overflow';
@ -20,11 +33,22 @@ export class OverflowNode extends ElementNode {
return new OverflowNode(node.__key);
}
static importJSON(serializedNode: SerializedOverflowNode): OverflowNode {
return $createOverflowNode();
}
constructor(key?: NodeKey): void {
super(key);
this.__type = 'overflow';
}
exportJSON(): SerializedElementNode {
return {
...super.exportJSON(),
type: 'overflow',
};
}
createDOM(config: EditorConfig): HTMLElement {
const div = document.createElement('span');
const className = config.theme.characterLimit;

View File

@ -6,12 +6,26 @@
*
*/
import type {EditorConfig, LexicalNode, NodeKey} from 'lexical';
import type {
EditorConfig,
LexicalNode,
NodeKey,
SerializedTextNode,
} from 'lexical';
import {Spread} from 'globals';
import {TextNode} from 'lexical';
export type SerializedEmojiNode = Spread<
{
className: string;
type: 'emoji';
},
SerializedTextNode
>;
export class EmojiNode extends TextNode {
__className?: string;
__className: string;
static getType(): string {
return 'emoji';
@ -47,6 +61,31 @@ export class EmojiNode extends TextNode {
super.updateDOM(prevNode, inner as HTMLElement, config);
return false;
}
static importJSON(serializedNode: SerializedEmojiNode): EmojiNode {
const node = $createEmojiNode(
serializedNode.className,
serializedNode.text,
);
node.setFormat(serializedNode.format);
node.setDetail(serializedNode.detail);
node.setMode(serializedNode.mode);
node.setStyle(serializedNode.style);
return node;
}
exportJSON(): SerializedEmojiNode {
return {
...super.exportJSON(),
className: this.getClassName(),
type: 'emoji',
};
}
getClassName(): string {
const self = this.getLatest<EmojiNode>();
return self.__className;
}
}
export function $isEmojiNode(

View File

@ -11,10 +11,12 @@ import type {
EditorConfig,
LexicalNode,
NodeKey,
SerializedLexicalNode,
} from 'lexical';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {mergeRegister} from '@lexical/utils';
import {Spread} from 'globals';
import {
$getNodeByKey,
COMMAND_PRIORITY_HIGH,
@ -114,6 +116,15 @@ function EquationComponent({
);
}
export type SerializedEquationNode = Spread<
{
type: 'equation';
equation: string;
inline: boolean;
},
SerializedLexicalNode
>;
export class EquationNode extends DecoratorNode<JSX.Element> {
__equation: string;
__inline: boolean;
@ -132,6 +143,23 @@ export class EquationNode extends DecoratorNode<JSX.Element> {
this.__inline = inline ?? false;
}
static importJSON(serializedNode: SerializedEquationNode): EquationNode {
const node = $createEquationNode(
serializedNode.equation,
serializedNode.inline,
);
return node;
}
exportJSON(): SerializedEquationNode {
return {
equation: this.getEquation(),
inline: this.__inline,
type: 'emoji',
version: 1,
};
}
exportDOM(): DOMExportOutput {
const element = document.createElement(this.__inline ? 'span' : 'div');
element.innerText = this.__equation;

View File

@ -7,11 +7,18 @@
*/
import type {ExcalidrawElementFragment} from './ExcalidrawModal';
import type {EditorConfig, LexicalEditor, LexicalNode, NodeKey} from 'lexical';
import type {
EditorConfig,
LexicalEditor,
LexicalNode,
NodeKey,
SerializedLexicalNode,
} from 'lexical';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {useLexicalNodeSelection} from '@lexical/react/useLexicalNodeSelection';
import {mergeRegister} from '@lexical/utils';
import {Spread} from 'globals';
import {
$getNodeByKey,
$getSelection,
@ -193,6 +200,15 @@ function ExcalidrawComponent({
);
}
export type SerializedExcalidrawNode = Spread<
{
data: string;
type: 'excalidraw';
version: 1;
},
SerializedLexicalNode
>;
export class ExcalidrawNode extends DecoratorNode<JSX.Element> {
__data: string;
@ -204,6 +220,18 @@ export class ExcalidrawNode extends DecoratorNode<JSX.Element> {
return new ExcalidrawNode(node.__data, node.__key);
}
static importJSON(serializedNode: SerializedExcalidrawNode): ExcalidrawNode {
return new ExcalidrawNode(serializedNode.data);
}
exportJSON(): SerializedExcalidrawNode {
return {
data: this.__data,
type: 'excalidraw',
version: 1,
};
}
constructor(data = '[]', key?: NodeKey) {
super(key);
this.__data = data;

View File

@ -12,6 +12,7 @@ import type {
LexicalEditor,
LexicalNode,
NodeKey,
SerializedLexicalNode,
} from 'lexical';
import './ImageNode.css';
@ -29,6 +30,7 @@ import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {TablePlugin} from '@lexical/react/LexicalTablePlugin';
import {useLexicalNodeSelection} from '@lexical/react/useLexicalNodeSelection';
import {mergeRegister} from '@lexical/utils';
import {Spread} from 'globals';
import {
$getNodeByKey,
$getSelection,
@ -310,6 +312,21 @@ function ImageComponent({
);
}
export type SerializedImageNode = Spread<
{
altText: string;
caption: LexicalEditor;
height?: number;
maxWidth: number;
showCaption: boolean;
src: string;
width?: number;
type: 'image';
version: 1;
},
SerializedLexicalNode
>;
export class ImageNode extends DecoratorNode<JSX.Element> {
__src: string;
__altText: string;
@ -336,6 +353,20 @@ export class ImageNode extends DecoratorNode<JSX.Element> {
);
}
static importJSON(serializedNode: SerializedImageNode): ImageNode {
const {altText, height, width, maxWidth, caption, src, showCaption} =
serializedNode;
return $createImageNode({
altText,
caption,
height,
maxWidth,
showCaption,
src,
width,
});
}
constructor(
src: string,
altText: string,
@ -363,6 +394,20 @@ export class ImageNode extends DecoratorNode<JSX.Element> {
return {element};
}
exportJSON(): SerializedImageNode {
return {
altText: this.getAltText(),
caption: this.__caption,
height: this.__height === 'inherit' ? 0 : this.__height,
maxWidth: this.__maxWidth,
showCaption: this.__showCaption,
src: this.getSrc(),
type: 'image',
version: 1,
width: this.__width === 'inherit' ? 0 : this.__width,
};
}
setWidthAndHeight(
width: 'inherit' | number,
height: 'inherit' | number,

View File

@ -6,10 +6,19 @@
*
*/
import type {EditorConfig, LexicalNode} from 'lexical';
import type {EditorConfig, LexicalNode, SerializedTextNode} from 'lexical';
import {Spread} from 'globals';
import {TextNode} from 'lexical';
export type SerializedKeywordNode = Spread<
{
type: 'keyword';
version: 1;
},
SerializedTextNode
>;
export class KeywordNode extends TextNode {
static getType(): string {
return 'keyword';
@ -19,6 +28,23 @@ export class KeywordNode extends TextNode {
return new KeywordNode(node.__text, node.__key);
}
static importJSON(serializedNode: SerializedKeywordNode): KeywordNode {
const node = $createKeywordNode(serializedNode.text);
node.setFormat(serializedNode.format);
node.setDetail(serializedNode.detail);
node.setMode(serializedNode.mode);
node.setStyle(serializedNode.style);
return node;
}
exportJSON(): SerializedKeywordNode {
return {
...super.exportJSON(),
type: 'keyword',
version: 1,
};
}
createDOM(config: EditorConfig): HTMLElement {
const dom = super.createDOM(config);
dom.style.cursor = 'default';

View File

@ -6,10 +6,25 @@
*
*/
import type {EditorConfig, LexicalNode, NodeKey} from 'lexical';
import type {
EditorConfig,
LexicalNode,
NodeKey,
SerializedTextNode,
} from 'lexical';
import {Spread} from 'globals';
import {TextNode} from 'lexical';
export type SerializedMentionNode = Spread<
{
mentionName: string;
type: 'mention';
version: 1;
},
SerializedTextNode
>;
const mentionStyle = 'background-color: rgba(24, 119, 232, 0.2)';
export class MentionNode extends TextNode {
__mention: string;
@ -21,12 +36,30 @@ export class MentionNode extends TextNode {
static clone(node: MentionNode): MentionNode {
return new MentionNode(node.__mention, node.__text, node.__key);
}
static importJSON(serializedNode: SerializedMentionNode): MentionNode {
const node = $createMentionNode(serializedNode.mentionName);
node.setTextContent(serializedNode.text);
node.setFormat(serializedNode.format);
node.setDetail(serializedNode.detail);
node.setMode(serializedNode.mode);
node.setStyle(serializedNode.style);
return node;
}
constructor(mentionName: string, text?: string, key?: NodeKey) {
super(text ?? mentionName, key);
this.__mention = mentionName;
}
exportJSON(): SerializedMentionNode {
return {
...super.exportJSON(),
mentionName: this.__mention,
type: 'mention',
version: 1,
};
}
createDOM(config: EditorConfig): HTMLElement {
const dom = super.createDOM(config);
dom.style.cssText = mentionStyle;

View File

@ -6,12 +6,13 @@
*
*/
import type {LexicalNode, NodeKey} from 'lexical';
import type {LexicalNode, NodeKey, SerializedLexicalNode} from 'lexical';
import './PollNode.css';
import {useCollaborationContext} from '@lexical/react/LexicalCollaborationPlugin';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {Spread} from 'globals';
import {$getNodeByKey, DecoratorNode} from 'lexical';
import * as React from 'react';
import {useMemo, useRef} from 'react';
@ -189,6 +190,16 @@ function PollComponent({
);
}
export type SerializedPollNode = Spread<
{
question: string;
options: Options;
type: 'poll';
version: 1;
},
SerializedLexicalNode
>;
export class PollNode extends DecoratorNode<JSX.Element> {
__question: string;
__options: Options;
@ -201,12 +212,27 @@ export class PollNode extends DecoratorNode<JSX.Element> {
return new PollNode(node.__question, node.__options, node.__key);
}
static importJSON(serializedNode: SerializedPollNode): PollNode {
const node = $createPollNode(serializedNode.question);
serializedNode.options.forEach(node.addOption);
return node;
}
constructor(question: string, options?: Options, key?: NodeKey) {
super(key);
this.__question = question;
this.__options = options || [createPollOption(), createPollOption()];
}
exportJSON(): SerializedPollNode {
return {
options: this.__options,
question: this.__question,
type: 'poll',
version: 1,
};
}
addOption(option: Option): void {
const self = this.getWritable<PollNode>();
const options = Array.from(self.__options);

View File

@ -6,7 +6,13 @@
*
*/
import type {EditorConfig, LexicalEditor, LexicalNode, NodeKey} from 'lexical';
import type {
EditorConfig,
LexicalEditor,
LexicalNode,
NodeKey,
SerializedLexicalNode,
} from 'lexical';
import './StickyNode.css';
@ -18,6 +24,7 @@ import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {LexicalNestedComposer} from '@lexical/react/LexicalNestedComposer';
import {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin';
import {Spread} from 'globals';
import {
$getNodeByKey,
$setSelection,
@ -266,10 +273,24 @@ function StickyComponent({
);
}
type StickyNoteColor = 'pink' | 'yellow';
export type SerializedStickyNode = Spread<
{
xOffset: number;
yOffset: number;
color: StickyNoteColor;
caption: LexicalEditor;
type: 'sticky';
version: 1;
},
SerializedLexicalNode
>;
export class StickyNode extends DecoratorNode<JSX.Element> {
__x: number;
__y: number;
__color: 'pink' | 'yellow';
__color: StickyNoteColor;
__caption: LexicalEditor;
static getType(): string {
@ -285,6 +306,14 @@ export class StickyNode extends DecoratorNode<JSX.Element> {
node.__key,
);
}
static importJSON(serializedNode: SerializedStickyNode): StickyNode {
return new StickyNode(
serializedNode.xOffset,
serializedNode.yOffset,
serializedNode.color,
serializedNode.caption,
);
}
constructor(
x: number,
@ -300,6 +329,17 @@ export class StickyNode extends DecoratorNode<JSX.Element> {
this.__color = color;
}
exportJSON(): SerializedStickyNode {
return {
caption: this.__caption,
color: this.__color,
type: 'sticky',
version: 1,
xOffset: this.__x,
yOffset: this.__y,
};
}
createDOM(config: EditorConfig): HTMLElement {
const div = document.createElement('div');
div.style.display = 'contents';

View File

@ -9,7 +9,11 @@
import type {ElementFormatType, LexicalNode, NodeKey} from 'lexical';
import {BlockWithAlignableContents} from '@lexical/react/LexicalBlockWithAlignableContents';
import {DecoratorBlockNode} from '@lexical/react/LexicalDecoratorBlockNode';
import {
DecoratorBlockNode,
SerializedDecoratorBlockNode,
} from '@lexical/react/LexicalDecoratorBlockNode';
import {Spread} from 'libdefs/globals';
import * as React from 'react';
import {useCallback, useEffect, useRef, useState} from 'react';
@ -42,6 +46,7 @@ function TweetComponent({
const createTweet = useCallback(async () => {
try {
// @ts-expect-error Twitter is attached to the window.
await window.twttr.widgets.createTweet(tweetID, containerRef.current);
setIsLoading(false);
@ -88,6 +93,15 @@ function TweetComponent({
);
}
export type SerializedTweetNode = Spread<
{
id: string;
type: 'tweet';
version: 1;
},
SerializedDecoratorBlockNode
>;
export class TweetNode extends DecoratorBlockNode<JSX.Element> {
__id: string;
@ -99,6 +113,21 @@ export class TweetNode extends DecoratorBlockNode<JSX.Element> {
return new TweetNode(node.__id, node.__format, node.__key);
}
static importJSON(serializedNode: SerializedTweetNode): TweetNode {
const node = $createTweetNode(serializedNode.id);
node.setFormat(serializedNode.format);
return node;
}
exportJSON(): SerializedTweetNode {
return {
...super.exportJSON(),
id: this.getId(),
type: 'tweet',
version: 1,
};
}
constructor(id: string, format?: ElementFormatType | null, key?: NodeKey) {
super(format, key);
this.__id = id;

View File

@ -6,10 +6,19 @@
*
*/
import type {EditorConfig} from 'lexical';
import type {EditorConfig, SerializedTextNode} from 'lexical';
import {Spread} from 'globals';
import {TextNode} from 'lexical';
export type SerializedTypeaheadNode = Spread<
{
type: 'typeahead';
version: 1;
},
SerializedTextNode
>;
export class TypeaheadNode extends TextNode {
static clone(node: TypeaheadNode): TypeaheadNode {
return new TypeaheadNode(node.__text, node.__key);
@ -19,6 +28,23 @@ export class TypeaheadNode extends TextNode {
return 'typeahead';
}
static importJSON(serializedNode: SerializedTypeaheadNode): TypeaheadNode {
const node = $createTypeaheadNode(serializedNode.text);
node.setFormat(serializedNode.format);
node.setDetail(serializedNode.detail);
node.setMode(serializedNode.mode);
node.setStyle(serializedNode.style);
return node;
}
exportJSON(): SerializedTypeaheadNode {
return {
...super.exportJSON(),
type: 'typeahead',
version: 1,
};
}
createDOM(config: EditorConfig): HTMLElement {
const dom = super.createDOM(config);
dom.style.cssText = 'color: #ccc;';

View File

@ -9,7 +9,11 @@
import type {ElementFormatType, LexicalNode, NodeKey} from 'lexical';
import {BlockWithAlignableContents} from '@lexical/react/LexicalBlockWithAlignableContents';
import {DecoratorBlockNode} from '@lexical/react/LexicalDecoratorBlockNode';
import {
DecoratorBlockNode,
SerializedDecoratorBlockNode,
} from '@lexical/react/LexicalDecoratorBlockNode';
import {Spread} from 'libdefs/globals';
import * as React from 'react';
type YouTubeComponentProps = Readonly<{
@ -34,6 +38,15 @@ function YouTubeComponent({format, nodeKey, videoID}: YouTubeComponentProps) {
);
}
export type SerializedYouTubeNode = Spread<
{
videoID: string;
type: 'youtube';
version: 1;
},
SerializedDecoratorBlockNode
>;
export class YouTubeNode extends DecoratorBlockNode<JSX.Element> {
__id: string;
@ -45,6 +58,21 @@ export class YouTubeNode extends DecoratorBlockNode<JSX.Element> {
return new YouTubeNode(node.__id, node.__format, node.__key);
}
static importJSON(serializedNode: SerializedYouTubeNode): YouTubeNode {
const node = $createYouTubeNode(serializedNode.videoID);
node.setFormat(serializedNode.format);
return node;
}
exportJSON(): SerializedYouTubeNode {
return {
...super.exportJSON(),
type: 'youtube',
version: 1,
videoID: this.__id,
};
}
constructor(id: string, format?: ElementFormatType | null, key?: NodeKey) {
super(format, key);
this.__id = id;

View File

@ -7,15 +7,25 @@
* @flow strict
*/
import type {ElementFormatType, LexicalNode, NodeKey} from 'lexical';
import type {
ElementFormatType,
LexicalNode,
NodeKey,
SerializedLexicalNode,
} from 'lexical';
import {DecoratorNode} from 'lexical';
export type SerializedDecoratorBlockNode = SerializedLexicalNode & {
format: ElementFormatType;
};
declare class DecoratorBlockNode<T> extends DecoratorNode<T> {
__format: ElementFormatType;
constructor(format?: ElementFormatType | null, key?: NodeKey);
createDOM(): HTMLElement;
setFormat(format: ElementFormatType): void;
exportJSON(): SerializedDecoratorBlockNode;
}
declare function $isDecoratorBlockNode<T>(

View File

@ -7,10 +7,21 @@
* @flow strict
*/
import type {ElementFormatType, LexicalNode, NodeKey} from 'lexical';
import type {
ElementFormatType,
LexicalNode,
NodeKey,
SerializedLexicalNode,
} from 'lexical';
import {DecoratorNode} from 'lexical';
export type SerializedDecoratorBlockNode = {
...SerializedLexicalNode,
format: ElementFormatType,
...
};
export class DecoratorBlockNode extends DecoratorNode<React$Node> {
__format: ?ElementFormatType;
@ -19,6 +30,14 @@ export class DecoratorBlockNode extends DecoratorNode<React$Node> {
this.__format = format;
}
exportJSON(): SerializedDecoratorBlockNode {
return {
format: this.__format || '',
type: 'decorator-block',
version: 1,
};
}
createDOM(): HTMLElement {
return document.createElement('div');
}

View File

@ -13,11 +13,17 @@ import type {
DOMExportOutput,
LexicalCommand,
LexicalNode,
SerializedLexicalNode,
} from 'lexical';
import {createCommand, DecoratorNode} from 'lexical';
import * as React from 'react';
export type SerializedHorizontalRuleNode = SerializedLexicalNode & {
type: 'horizontalrule',
version: 1,
};
export const INSERT_HORIZONTAL_RULE_COMMAND: LexicalCommand<void> =
createCommand();
@ -47,6 +53,19 @@ export class HorizontalRuleNode extends DecoratorNode<React$Node> {
return {element: document.createElement('hr')};
}
static importJSON(
serializedNode: SerializedHorizontalRuleNode,
): HorizontalRuleNode {
return $createHorizontalRuleNode();
}
exportJSON(): SerializedLexicalNode {
return {
type: 'horizontalrule',
version: 1,
};
}
createDOM(): HTMLElement {
const div = document.createElement('div');
div.style.display = 'contents';

View File

@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*
*/
import type {
DOMConversionMap,
EditorConfig,
@ -13,8 +14,11 @@ import type {
NodeKey,
ParagraphNode,
LexicalEditor,
SerializedElementNode,
} from 'lexical';
import {ElementNode} from 'lexical';
import {Spread} from 'libdefs/globals';
export type InitialEditorStateType = null | string | EditorState | (() => void);
export declare class QuoteNode extends ElementNode {
@ -25,6 +29,7 @@ export declare class QuoteNode extends ElementNode {
updateDOM(prevNode: QuoteNode, dom: HTMLElement): boolean;
insertNewAfter(): ParagraphNode;
collapseAtStart(): true;
importJSON(serializedNode: SerializedQuoteNode): QuoteNode;
}
export function $createQuoteNode(): QuoteNode;
export function $isQuoteNode(
@ -42,7 +47,9 @@ export declare class HeadingNode extends ElementNode {
static importDOM(): DOMConversionMap | null;
insertNewAfter(): ParagraphNode;
collapseAtStart(): true;
importJSON(serializedNode: SerializedHeadingNode): QuoteNode;
}
export function $createHeadingNode(headingTag: HeadingTagType): HeadingNode;
export function $isHeadingNode(
node: LexicalNode | null | undefined,
@ -51,3 +58,20 @@ export function registerRichText(
editor: LexicalEditor,
initialEditorState?: InitialEditorStateType,
): () => void;
export type SerializedHeadingNode = Spread<
{
tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5';
type: 'heading';
version: 1;
},
SerializedElementNode
>;
export type SerializedQuoteNode = Spread<
{
type: 'quote';
version: 1;
},
SerializedElementNode
>;

View File

@ -16,6 +16,7 @@ import type {
LexicalNode,
NodeKey,
ParagraphNode,
SerializedElementNode,
TextFormatType,
} from 'lexical';
@ -33,6 +34,7 @@ import {
addClassNamesToElement,
mergeRegister,
} from '@lexical/utils';
import {Spread} from 'globals';
import {
$createParagraphNode,
$getRoot,
@ -72,6 +74,23 @@ import {CAN_USE_BEFORE_INPUT, IS_IOS, IS_SAFARI} from 'shared-ts/environment';
export type InitialEditorStateType = null | string | EditorState | (() => void);
export type SerializedHeadingNode = Spread<
{
tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5';
type: 'heading';
version: 1;
},
SerializedElementNode
>;
export type SerializedQuoteNode = Spread<
{
type: 'quote';
version: 1;
},
SerializedElementNode
>;
// Convoluted logic to make this work with Flow. Order matters.
const options = {tag: 'history-merge'};
const setEditorOptions: {
@ -107,6 +126,21 @@ export class QuoteNode extends ElementNode {
return false;
}
static importJSON(serializedNode: SerializedQuoteNode): QuoteNode {
const node = $createQuoteNode();
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction);
return node;
}
exportJSON(): SerializedElementNode {
return {
...super.exportJSON(),
type: 'quote',
};
}
// Mutation
insertNewAfter(): ParagraphNode {
@ -202,6 +236,23 @@ export class HeadingNode extends ElementNode {
};
}
static importJSON(serializedNode: SerializedHeadingNode): HeadingNode {
const node = $createHeadingNode(serializedNode.tag);
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction);
return node;
}
exportJSON(): SerializedHeadingNode {
return {
...super.exportJSON(),
tag: this.__tag,
type: 'heading',
version: 1,
};
}
// Mutation
insertNewAfter(): ParagraphNode {

View File

@ -15,6 +15,8 @@ import type {
LexicalEditor,
LexicalNode,
NodeKey,
SerializedElementNode,
SerializedGridCellNode,
} from 'lexical';
import {addClassNamesToElement} from '@lexical/utils';
@ -34,6 +36,14 @@ export const TableCellHeaderStates = {
export type TableCellHeaderState = $Values<typeof TableCellHeaderStates>;
export type SerializedTableCellNode = {
...SerializedGridCellNode,
headerState: TableCellHeaderState,
type: 'tablecell',
width: number,
...
};
export class TableCellNode extends GridCellNode {
__headerState: TableCellHeaderState;
__width: ?number;
@ -64,6 +74,14 @@ export class TableCellNode extends GridCellNode {
};
}
static importJSON(serializedNode: SerializedTableCellNode): TableCellNode {
return $createTableCellNode(
serializedNode.headerState,
serializedNode.colSpan,
serializedNode.width,
);
}
constructor(
headerState?: TableCellHeaderState = TableCellHeaderStates.NO_STATUS,
colSpan?: number = 1,
@ -115,6 +133,15 @@ export class TableCellNode extends GridCellNode {
};
}
exportJSON(): SerializedElementNode {
return {
...super.exportJSON(),
headerState: this.__headerState,
width: this.getWidth(),
type: 'tablecell',
};
}
getTag(): string {
return this.hasHeader() ? 'th' : 'td';
}

View File

@ -17,6 +17,7 @@ import type {
LexicalEditor,
LexicalNode,
NodeKey,
SerializedElementNode,
} from 'lexical';
import {addClassNamesToElement} from '@lexical/utils';
@ -27,6 +28,13 @@ import {$isTableCellNode} from './LexicalTableCellNode';
import {$isTableRowNode} from './LexicalTableRowNode';
import {getTableGrid} from './LexicalTableSelectionHelpers';
export type SerializedTableNode = {
...SerializedElementNode,
type: 'table',
version: 1,
...
};
export class TableNode extends GridNode {
__grid: ?Grid;
@ -47,10 +55,22 @@ export class TableNode extends GridNode {
};
}
static importJSON(serializedNode: SerializedTableNode): TableNode {
return $createTableNode();
}
constructor(key?: NodeKey): void {
super(key);
}
exportJSON(): SerializedElementNode {
return {
...super.exportJSON(),
type: 'table',
version: 1,
};
}
createDOM(config: EditorConfig, editor: LexicalEditor): HTMLElement {
const tableElement = document.createElement('table');

View File

@ -13,11 +13,20 @@ import type {
EditorConfig,
LexicalNode,
NodeKey,
SerializedElementNode,
} from 'lexical';
import {addClassNamesToElement} from '@lexical/utils';
import {GridRowNode} from 'lexical';
export type SerializedTableRowNode = {
...SerializedElementNode,
height: number,
type: 'tablerow',
version: 1,
...
};
export class TableRowNode extends GridRowNode {
__height: ?number;
@ -38,11 +47,23 @@ export class TableRowNode extends GridRowNode {
};
}
static importJSON(serializedNode: SerializedTableRowNode): TableRowNode {
return $createTableRowNode(serializedNode.height);
}
constructor(height?: ?number, key?: NodeKey): void {
super(key);
this.__height = height;
}
exportJSON(): SerializedElementNode {
return {
...super.exportJSON(),
type: 'tablerow',
version: 1,
};
}
createDOM(config: EditorConfig): HTMLElement {
const element = document.createElement('tr');

View File

@ -7,6 +7,7 @@
*/
import {Class, $ReadOnly} from 'utility-types';
import {Spread} from 'globals';
/**
* LexicalCommands
@ -161,6 +162,10 @@ export declare class LexicalEditor {
parseEditorState(
maybeStringifiedEditorState: string | ParsedEditorState,
): EditorState;
unstable_parseEditorState(
maybeStringifiedEditorState: string | SerializedEditorState,
updateFn?: () => void,
): EditorState;
update(updateFn: () => void, options?: EditorUpdateOptions): boolean;
focus(callbackFn?: () => void): void;
blur(): void;
@ -291,6 +296,7 @@ export interface EditorState {
isEmpty(): boolean;
read<V>(callbackFn: () => V): V;
toJSON(space?: string | number): JSONEditorState;
unstable_toJSON(): SerializedEditorState;
clone(
selection?: RangeSelection | NodeSelection | GridSelection | null,
): EditorState;
@ -605,7 +611,10 @@ export declare class TextNode extends LexicalNode {
toggleFormat(type: TextFormatType): TextNode;
toggleDirectionless(): TextNode;
toggleUnmergeable(): TextNode;
setMode(type: TextModeType): TextNode;
setMode(type: TextModeType): this;
setDetail(detail: number): TextNode;
getDetail(): number;
getMode(): TextModeType;
setTextContent(text: string): TextNode;
select(_anchorOffset?: number, _focusOffset?: number): RangeSelection;
spliceText(
@ -619,6 +628,8 @@ export declare class TextNode extends LexicalNode {
splitText(...splitOffsets: Array<number>): Array<TextNode>;
mergeWithSibling(target: TextNode): TextNode;
isTextEntity(): boolean;
static importJSON(serializedTextNode: SerializedTextNode): TextNode;
exportJSON(): SerializedTextNode;
}
export function $createTextNode(text?: string): TextNode;
export function $isTextNode(
@ -635,6 +646,10 @@ export declare class LineBreakNode extends LexicalNode {
getTextContent(): '\n';
createDOM(): HTMLElement;
updateDOM(): false;
static importJSON(
serializedLineBreakNode: SerializedLexicalNode,
): LineBreakNode;
exportJSON(): SerializedLexicalNode;
}
export function $createLineBreakNode(): LineBreakNode;
export function $isLineBreakNode(
@ -658,6 +673,8 @@ export declare class RootNode extends ElementNode {
updateDOM(prevNode: RootNode, dom: HTMLElement): false;
append(...nodesToAppend: Array<LexicalNode>): ElementNode;
canBeEmpty(): false;
static importJSON(serializedRootNode: SerializedRootNode): RootNode;
exportJSON(): SerializedElementNode;
}
export function $isRootNode(
node: LexicalNode | null | undefined,
@ -674,6 +691,7 @@ export declare class ElementNode extends LexicalNode {
__dir: 'ltr' | 'rtl' | null;
constructor(key?: NodeKey);
getFormat(): number;
getFormatType(): 'left' | 'center' | 'right' | 'justify';
getIndent(): number;
getChildren<T extends Array<LexicalNode>>(): T;
getChildrenKeys(): Array<NodeKey>;
@ -722,6 +740,7 @@ export declare class ElementNode extends LexicalNode {
deleteCount: number,
nodesToInsert: Array<LexicalNode>,
): ElementNode;
exportJSON(): SerializedElementNode;
}
export function $isElementNode(
node: LexicalNode | null | undefined,
@ -751,6 +770,10 @@ export declare class ParagraphNode extends ElementNode {
updateDOM(prevNode: ParagraphNode, dom: HTMLElement): boolean;
insertNewAfter(): ParagraphNode;
collapseAtStart(): boolean;
static importJSON(
serializedParagraphNode: SerializedElementNode,
): ParagraphNode;
exportJSON(): SerializedElementNode;
}
export function $createParagraphNode(): ParagraphNode;
export function $isParagraphNode(
@ -797,3 +820,49 @@ export function $getDecoratorNode(
* LexicalVersion
*/
export declare var VERSION: string;
// Serialization
export type SerializedLexicalNode = {
type: string;
version: number;
};
export type SerializedTextNode = Spread<
{
detail: number;
format: number;
mode: TextModeType;
style: string;
text: string;
},
SerializedLexicalNode
>;
export type SerializedElementNode = Spread<
{
children: Array<SerializedLexicalNode>;
direction: 'ltr' | 'rtl' | null;
format: 'left' | 'center' | 'right' | 'justify';
indent: number;
},
SerializedLexicalNode
>;
export type SerializedRootNode = Spread<
{
type: 'root';
},
SerializedElementNode
>;
export type SerializedGridCellNode = Spread<
{
colSpan: number;
},
SerializedElementNode
>;
export interface SerializedEditorState {
root: SerializedRootNode;
}

View File

@ -167,6 +167,10 @@ declare export class LexicalEditor {
parseEditorState(
maybeStringifiedEditorState: string | ParsedEditorState,
): EditorState;
unstable_parseEditorState<SerializedNode>(
maybeStringifiedEditorState: string | SerializedEditorState<SerializedNode>,
updateFn?: () => void,
): EditorState;
update(updateFn: () => void, options?: EditorUpdateOptions): boolean;
focus(callbackFn?: () => void): void;
blur(): void;
@ -292,6 +296,7 @@ export interface EditorState {
isEmpty(): boolean;
read<V>(callbackFn: () => V): V;
toJSON(space?: string | number): JSONEditorState;
unstable_toJSON<SerializedNode>(): SerializedEditorState<SerializedNode>;
clone(
selection?: RangeSelection | NodeSelection | GridSelection | null,
): EditorState;
@ -631,6 +636,9 @@ declare export class TextNode extends LexicalNode {
toggleDirectionless(): this;
toggleUnmergeable(): this;
setMode(type: TextModeType): this;
setDetail(detail: number): this;
getDetail(): number;
getMode(): TextModeType;
setTextContent(text: string): TextNode;
select(_anchorOffset?: number, _focusOffset?: number): RangeSelection;
spliceText(
@ -644,6 +652,8 @@ declare export class TextNode extends LexicalNode {
splitText(...splitOffsets: Array<number>): Array<TextNode>;
mergeWithSibling(target: TextNode): TextNode;
isTextEntity(): boolean;
static importJSON(serializedTextNode: SerializedTextNode): TextNode;
exportJSON(): SerializedTextNode;
}
declare export function $createTextNode(text?: string): TextNode;
declare export function $isTextNode(
@ -661,6 +671,10 @@ declare export class LineBreakNode extends LexicalNode {
getTextContent(): '\n';
createDOM(): HTMLElement;
updateDOM(): false;
static importJSON(
serializedLineBreakNode: SerializedLineBreakNode,
): LineBreakNode;
exportJSON(): SerializedLexicalNode;
}
declare export function $createLineBreakNode(): LineBreakNode;
declare export function $isLineBreakNode(
@ -693,7 +707,7 @@ declare export function $isRootNode(
/**
* LexicalElementNode
*/
export type ElementFormatType = 'left' | 'center' | 'right' | 'justify';
export type ElementFormatType = 'left' | 'center' | 'right' | 'justify' | '';
declare export class ElementNode extends LexicalNode {
__children: Array<NodeKey>;
__format: number;
@ -701,6 +715,7 @@ declare export class ElementNode extends LexicalNode {
__dir: 'ltr' | 'rtl' | null;
constructor(key?: NodeKey): void;
getFormat(): number;
getFormatType(): ElementFormatType;
getIndent(): number;
getChildren<T: Array<LexicalNode>>(): T;
getChildrenKeys(): Array<NodeKey>;
@ -749,6 +764,7 @@ declare export class ElementNode extends LexicalNode {
deleteCount: number,
nodesToInsert: Array<LexicalNode>,
): ElementNode;
exportJSON(): SerializedElementNode;
}
declare export function $isElementNode(
node: ?LexicalNode,
@ -779,6 +795,10 @@ declare export class ParagraphNode extends ElementNode {
updateDOM(prevNode: ParagraphNode, dom: HTMLElement): boolean;
insertNewAfter(): ParagraphNode;
collapseAtStart(): boolean;
static importJSON(
serializedParagraphNode: SerializedParagraphNode,
): ParagraphNode;
exportJSON(): SerializedElementNode;
}
declare export function $createParagraphNode(): ParagraphNode;
declare export function $isParagraphNode(
@ -843,3 +863,58 @@ export type EventHandler = (
* LexicalVersion
*/
declare export var VERSION: string;
// Serialization
export type SerializedLexicalNode = {
type: string,
version: number,
...
};
export type SerializedTextNode = {
...SerializedLexicalNode,
detail: number,
format: number,
mode: TextModeType,
style: string,
text: string,
...
};
export type SerializedElementNode = {
...SerializedLexicalNode,
children: Array<SerializedLexicalNode>,
direction: 'ltr' | 'rtl' | null,
format: ElementFormatType,
indent: number,
...
};
export type SerializedParagraphNode = {
...SerializedElementNode,
type: 'paragraph',
...
};
export type SerializedLineBreakNode = {
...SerializedLexicalNode,
type: 'linebreak',
...
};
export type SerializedRootNode = {
...SerializedElementNode,
type: 'root',
...
};
export type SerializedGridCellNode = {
...SerializedElementNode,
colSpan: number,
...
};
export interface SerializedEditorState<SerializedNode> {
root: SerializedRootNode<SerializedNode>;
}

View File

@ -85,9 +85,23 @@ export const ELEMENT_TYPE_TO_FORMAT: {[ElementFormatType]: number} = {
right: IS_ALIGN_RIGHT,
};
export const ELEMENT_FORMAT_TO_TYPE: {[number]: ElementFormatType} = {
[IS_ALIGN_CENTER]: 'center',
[IS_ALIGN_JUSTIFY]: 'justify',
[IS_ALIGN_LEFT]: 'left',
[IS_ALIGN_RIGHT]: 'right',
};
export const TEXT_MODE_TO_TYPE: {[TextModeType]: 0 | 1 | 2 | 3} = {
inert: IS_INERT,
normal: IS_NORMAL,
segmented: IS_SEGMENTED,
token: IS_TOKEN,
};
export const TEXT_TYPE_TO_MODE: {[number]: TextModeType} = {
[IS_INERT]: 'inert',
[IS_NORMAL]: 'normal',
[IS_SEGMENTED]: 'segmented',
[IS_TOKEN]: 'token',
};

View File

@ -7,7 +7,11 @@
* @flow strict
*/
import type {EditorState, ParsedEditorState} from './LexicalEditorState';
import type {
EditorState,
ParsedEditorState,
SerializedEditorState,
} from './LexicalEditorState';
import type {DOMConversion, LexicalNode, NodeKey} from './LexicalNode';
import getDOMSelection from 'shared/getDOMSelection';
@ -22,6 +26,7 @@ import {
commitPendingUpdates,
parseEditorState,
triggerListeners,
unstable_parseEditorState,
updateEditor,
} from './LexicalUpdates';
import {
@ -575,6 +580,7 @@ export class LexicalEditor {
}
commitPendingUpdates(this);
}
// TODO: once unstable_parseEditorState is stable, swap that for this.
parseEditorState(
maybeStringifiedEditorState: string | ParsedEditorState,
): EditorState {
@ -584,6 +590,16 @@ export class LexicalEditor {
: maybeStringifiedEditorState;
return parseEditorState(parsedEditorState, this);
}
unstable_parseEditorState(
maybeStringifiedEditorState: string | SerializedEditorState,
updateFn?: () => void,
): EditorState {
const serializedEditorState =
typeof maybeStringifiedEditorState === 'string'
? JSON.parse(maybeStringifiedEditorState)
: maybeStringifiedEditorState;
return unstable_parseEditorState(serializedEditorState, this, updateFn);
}
update(updateFn: () => void, options?: EditorUpdateOptions): void {
updateEditor(this, updateFn, options);
}

View File

@ -15,25 +15,34 @@ import type {
NodeSelection,
RangeSelection,
} from './LexicalSelection';
import type {SerializedRootNode} from './nodes/LexicalRootNode';
import invariant from '../../shared/src/invariant';
import {$isElementNode} from '.';
import {
$isGridSelection,
$isNodeSelection,
$isRangeSelection,
} from './LexicalSelection';
import {readEditorState} from './LexicalUpdates';
import {$getRoot} from './LexicalUtils';
import {$createRootNode} from './nodes/LexicalRootNode';
// TODO: deprecated
export type ParsedEditorState = {
_nodeMap: Array<[NodeKey, ParsedNode]>,
_selection: null | ParsedSelection,
};
// TODO: deprecated
export type JSONEditorState = {
_nodeMap: Array<[NodeKey, LexicalNode]>,
_selection: null | ParsedSelection,
};
export interface SerializedEditorState {
root: SerializedRootNode;
}
export function editorStateHasDirtySelection(
editorState: EditorState,
editor: LexicalEditor,
@ -59,6 +68,35 @@ export function createEmptyEditorState(): EditorState {
return new EditorState(new Map([['root', $createRootNode()]]));
}
function exportNodeToJSON<SerializedNode>(node: LexicalNode): SerializedNode {
const serializedNode = node.exportJSON();
const nodeClass = node.constructor;
if (serializedNode.type !== nodeClass.getType()) {
invariant(
false,
'LexicalNode: Node %s does not implement .exportJSON().',
nodeClass.name,
);
}
const serializedChildren = serializedNode.children;
if ($isElementNode(node)) {
if (!Array.isArray(serializedChildren)) {
invariant(
false,
'LexicalNode: Node %s is an element but .exportJSON() does not have a children array.',
nodeClass.name,
);
}
const children = node.getChildren();
for (let i = 0; i < children.length; i++) {
const child = children[i];
const serializedChildNode = exportNodeToJSON(child);
serializedChildren.push(serializedChildNode);
}
}
return serializedNode;
}
export class EditorState {
_nodeMap: NodeMap;
_selection: null | RangeSelection | NodeSelection | GridSelection;
@ -90,6 +128,7 @@ export class EditorState {
editorState._readOnly = true;
return editorState;
}
// TODO: remove when we use the other toJSON
toJSON(space?: string | number): JSONEditorState {
const selection = this._selection;
@ -132,4 +171,9 @@ export class EditorState {
: null,
};
}
unstable_toJSON(): SerializedEditorState {
return readEditorState(this, () => ({
root: exportNodeToJSON($getRoot()),
}));
}
}

View File

@ -43,6 +43,12 @@ import {
export type NodeMap = Map<NodeKey, LexicalNode>;
export type SerializedLexicalNode = {
type: string,
version: number,
...
};
export function removeNode(
nodeToRemove: LexicalNode,
restoreSelection: boolean,
@ -600,6 +606,22 @@ export class LexicalNode {
return null;
}
// $FlowFixMe: Intentional work around for Flow
exportJSON(): Object {
invariant(false, 'exportJSON: base method not extended');
}
static importJSON(
// $FlowFixMe: Intentional work around for Flow
serializedNode: Object,
): LexicalNode {
invariant(
false,
'LexicalNode: Node %s does not implement .importJSON().',
this.name,
);
}
// Setters and mutators
remove(preserveEmptyParent?: boolean): void {

View File

@ -12,19 +12,24 @@ import type {
LexicalCommand,
LexicalEditor,
MutatedNodes,
RegisteredNodes,
Transform,
} from './LexicalEditor';
import type {ParsedEditorState} from './LexicalEditorState';
import type {
ParsedEditorState,
SerializedEditorState,
} from './LexicalEditorState';
import type {LexicalNode} from './LexicalNode';
import type {NodeParserState, ParsedNode} from './LexicalParsing';
import invariant from 'shared/invariant';
import {$isTextNode} from '.';
import {$isElementNode, $isTextNode} from '.';
import {FULL_RECONCILE, NO_DIRTY_NODES} from './LexicalConstants';
import {resetEditor} from './LexicalEditor';
import {
cloneEditorState,
createEmptyEditorState,
EditorState,
editorStateHasDirtySelection,
} from './LexicalEditorState';
@ -242,6 +247,7 @@ function $applyAllTransforms(
editor._dirtyElements = dirtyElements;
}
// TODO: once unstable_parseEditorState is stable, swap that for this.
export function parseEditorState(
parsedEditorState: ParsedEditorState,
editor: LexicalEditor,
@ -279,6 +285,77 @@ export function parseEditorState(
return editorState;
}
type InternalSerializedNode = {
children?: Array<InternalSerializedNode>,
type: string,
version: number,
};
function parseSerializedNode<SerializedNode: InternalSerializedNode>(
serializedNode: SerializedNode,
registeredNodes: RegisteredNodes,
): LexicalNode {
const type = serializedNode.type;
const registeredNode = registeredNodes.get(type);
if (registeredNode === undefined) {
invariant(false, 'parseEditorState: type "%s" + not found', type);
}
const nodeClass = registeredNode.klass;
if (serializedNode.type !== nodeClass.getType()) {
invariant(
false,
'LexicalNode: Node %s does not implement .importJSON().',
nodeClass.name,
);
}
const node = nodeClass.importJSON(serializedNode);
const children = serializedNode.children;
if ($isElementNode(node) && Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
const serializedJSONChildNode = children[i];
const childNode = parseSerializedNode(
serializedJSONChildNode,
registeredNodes,
);
node.append(childNode);
}
}
return node;
}
export function unstable_parseEditorState(
serializedEditorState: SerializedEditorState,
editor: LexicalEditor,
updateFn: void | (() => void),
): EditorState {
const editorState = createEmptyEditorState();
const previousActiveEditorState = activeEditorState;
const previousReadOnlyMode = isReadOnlyMode;
const previousActiveEditor = activeEditor;
activeEditorState = editorState;
isReadOnlyMode = false;
activeEditor = editor;
try {
const registeredNodes = editor._nodes;
// $FlowFixMe: intentional cast to our internal type
const serializedNode: InternalSerializedNode = serializedEditorState.root;
parseSerializedNode(serializedNode, registeredNodes);
if (updateFn) {
updateFn();
}
// Make the editorState immutable
editorState._readOnly = true;
if (__DEV__) {
handleDEVOnlyPendingUpdateGuarantees(editorState);
}
} finally {
activeEditorState = previousActiveEditorState;
isReadOnlyMode = previousReadOnlyMode;
activeEditor = previousActiveEditor;
}
return editorState;
}
// This technically isn't an update but given we need
// exposure to the module's active bindings, we have this
// function here

View File

@ -1006,6 +1006,26 @@ describe('LexicalEditor tests', () => {
});
});
it('exportJSON API - parses parsed JSON', async () => {
await update(() => {
const paragraph = $createParagraphNode();
originalText = $createTextNode('Hello world');
originalText.select(6, 11);
paragraph.append(originalText);
$getRoot().append(paragraph);
});
const stringifiedEditorState = JSON.stringify(
editor.getEditorState().unstable_toJSON(),
);
const parsedEditorStateFromObject = editor.unstable_parseEditorState(
JSON.parse(stringifiedEditorState),
);
parsedEditorStateFromObject.read(() => {
const root = $getRoot();
expect(root.getTextContent()).toMatch(/Hello world/);
});
});
describe('range selection', () => {
beforeEach(async () => {
init();

View File

@ -6,7 +6,7 @@
*
* @flow strict
*/
import type {NodeKey} from '../LexicalNode';
import type {NodeKey, SerializedLexicalNode} from '../LexicalNode';
import type {
GridSelection,
NodeSelection,
@ -17,7 +17,11 @@ import type {
import invariant from 'shared/invariant';
import {$isRootNode, $isTextNode, TextNode} from '../';
import {DOUBLE_LINE_BREAK, ELEMENT_TYPE_TO_FORMAT} from '../LexicalConstants';
import {
DOUBLE_LINE_BREAK,
ELEMENT_FORMAT_TO_TYPE,
ELEMENT_TYPE_TO_FORMAT,
} from '../LexicalConstants';
import {LexicalNode} from '../LexicalNode';
import {
$getSelection,
@ -32,7 +36,16 @@ import {
removeFromParent,
} from '../LexicalUtils';
export type ElementFormatType = 'left' | 'center' | 'right' | 'justify';
export type SerializedElementNode = {
...SerializedLexicalNode,
children: Array<SerializedLexicalNode>,
direction: 'ltr' | 'rtl' | null,
format: 'left' | 'center' | 'right' | 'justify' | '',
indent: number,
...
};
export type ElementFormatType = 'left' | 'center' | 'right' | 'justify' | '';
export class ElementNode extends LexicalNode {
__children: Array<NodeKey>;
@ -52,6 +65,10 @@ export class ElementNode extends LexicalNode {
const self = this.getLatest();
return self.__format;
}
getFormatType(): 'left' | 'center' | 'right' | 'justify' | '' {
const format = this.getFormat();
return ELEMENT_FORMAT_TO_TYPE[format] || '';
}
getIndent(): number {
const self = this.getLatest();
return self.__indent;
@ -283,7 +300,7 @@ export class ElementNode extends LexicalNode {
setFormat(type: ElementFormatType): this {
errorOnReadOnly();
const self = this.getWritable();
self.__format = ELEMENT_TYPE_TO_FORMAT[type];
self.__format = ELEMENT_TYPE_TO_FORMAT[type] || 0;
return this;
}
setIndent(indentLevel: number): this {
@ -410,6 +427,17 @@ export class ElementNode extends LexicalNode {
return writableSelf;
}
// JSON serialization
exportJSON(): SerializedElementNode {
return {
children: [],
direction: this.getDirection(),
format: this.getFormatType(),
indent: this.getIndent(),
type: 'element',
version: 1,
};
}
// These are intended to be extends for specific element heuristics.
insertNewAfter(selection: RangeSelection): null | LexicalNode {
return null;

View File

@ -7,16 +7,29 @@
* @flow strict
*/
import type {LexicalNode, NodeKey} from 'lexical';
import type {LexicalNode, NodeKey, SerializedElementNode} from 'lexical';
import {ElementNode} from './LexicalElementNode';
export type SerializedGridCellNode = {
...SerializedElementNode,
colSpan: number,
...
};
export class GridCellNode extends ElementNode {
__colSpan: number;
constructor(colSpan: number, key?: NodeKey) {
super(key);
}
exportJSON(): SerializedElementNode {
return {
...super.exportJSON(),
colSpan: this.__colSpan,
};
}
}
export function $isGridCellNode(node: ?LexicalNode): boolean %checks {

View File

@ -11,10 +11,17 @@ import type {
DOMConversionMap,
DOMConversionOutput,
NodeKey,
SerializedLexicalNode,
} from '../LexicalNode';
import {LexicalNode} from '../LexicalNode';
export type SerializedLineBreakNode = {
...SerializedLexicalNode,
type: 'linebreak',
...
};
export class LineBreakNode extends LexicalNode {
static getType(): string {
return 'linebreak';
@ -48,6 +55,19 @@ export class LineBreakNode extends LexicalNode {
}),
};
}
static importJSON(
serializedLineBreakNode: SerializedLineBreakNode,
): LineBreakNode {
return $createLineBreakNode();
}
exportJSON(): SerializedLexicalNode {
return {
type: 'linebreak',
version: 1,
};
}
}
function convertLineBreakElement(node: Node): DOMConversionOutput {

View File

@ -17,11 +17,19 @@ import type {
DOMExportOutput,
LexicalNode,
} from '../LexicalNode';
import type {SerializedElementNode} from './LexicalElementNode';
import {getCachedClassNameArray} from '../LexicalUtils';
import {ElementNode} from './LexicalElementNode';
import {$isTextNode} from './LexicalTextNode';
export type SerializedParagraphNode = {
...SerializedElementNode,
type: 'paragraph',
version: 1,
...
};
export class ParagraphNode extends ElementNode {
static getType(): string {
return 'paragraph';
@ -72,6 +80,22 @@ export class ParagraphNode extends ElementNode {
};
}
static importJSON(serializedNode: SerializedParagraphNode): ParagraphNode {
const node = $createParagraphNode();
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction);
return node;
}
exportJSON(): SerializedElementNode {
return {
...super.exportJSON(),
type: 'paragraph',
version: 1,
};
}
// Mutation
insertNewAfter(): ParagraphNode {

View File

@ -9,14 +9,21 @@
import type {LexicalNode} from '../LexicalNode';
import type {ParsedElementNode} from '../LexicalParsing';
import type {SerializedElementNode} from './LexicalElementNode';
import invariant from 'shared/invariant';
import {NO_DIRTY_NODES} from '../LexicalConstants';
import {getActiveEditor, isCurrentlyReadOnlyMode} from '../LexicalUpdates';
import {$getRoot} from '../LexicalUtils';
import {$isDecoratorNode} from './LexicalDecoratorNode';
import {$isElementNode, ElementNode} from './LexicalElementNode';
export type SerializedRootNode = {
...SerializedElementNode,
...
};
export class RootNode extends ElementNode {
__cachedText: null | string;
@ -93,6 +100,26 @@ export class RootNode extends ElementNode {
return super.append(...nodesToAppend);
}
static importJSON(serializedNode: SerializedRootNode): RootNode {
// We don't create a root, and instead use the existing root.
const node = $getRoot();
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction);
return node;
}
exportJSON(): SerializedRootNode {
return {
children: [],
direction: this.getDirection(),
format: this.getFormatType(),
indent: this.getIndent(),
type: 'root',
version: 1,
};
}
// TODO: Deprecated
toJSON(): ParsedElementNode {
return {
__children: this.__children,

View File

@ -12,6 +12,7 @@ import type {
DOMConversionMap,
DOMConversionOutput,
NodeKey,
SerializedLexicalNode,
} from '../LexicalNode';
import type {
GridSelection,
@ -37,6 +38,7 @@ import {
IS_UNMERGEABLE,
TEXT_MODE_TO_TYPE,
TEXT_TYPE_TO_FORMAT,
TEXT_TYPE_TO_MODE,
} from '../LexicalConstants';
import {LexicalNode} from '../LexicalNode';
import {
@ -55,6 +57,16 @@ import {
toggleTextFormatType,
} from '../LexicalUtils';
export type SerializedTextNode = {
...SerializedLexicalNode,
detail: number,
format: number,
mode: TextModeType,
style: string,
text: string,
...
};
export type TextFormatType =
| 'bold'
| 'underline'
@ -264,6 +276,16 @@ export class TextNode extends LexicalNode {
return self.__format;
}
getDetail(): number {
const self = this.getLatest();
return self.__detail;
}
getMode(): TextModeType {
const self = this.getLatest();
return TEXT_TYPE_TO_MODE[self.__mode];
}
getStyle(): string {
const self = this.getLatest();
return self.__style;
@ -447,6 +469,27 @@ export class TextNode extends LexicalNode {
};
}
static importJSON(serializedNode: SerializedTextNode): TextNode {
const node = $createTextNode(serializedNode.text);
node.setFormat(serializedNode.format);
node.setDetail(serializedNode.detail);
node.setMode(serializedNode.mode);
node.setStyle(serializedNode.style);
return node;
}
exportJSON(): SerializedTextNode {
return {
detail: this.getDetail(),
format: this.getFormat(),
mode: this.getMode(),
style: this.getStyle(),
text: this.getTextContent(),
type: 'text',
version: 1,
};
}
// Mutators
selectionTransform(
prevSelection: null | RangeSelection | NodeSelection | GridSelection,
@ -460,6 +503,13 @@ export class TextNode extends LexicalNode {
return self;
}
setDetail(detail: number): this {
errorOnReadOnly();
const self = this.getWritable();
self.__detail = detail;
return self;
}
setStyle(style: string): this {
errorOnReadOnly();
const self = this.getWritable();

View File

@ -82,6 +82,26 @@ describe('LexicalElementNode tests', () => {
});
}
describe('exportJSON()', () => {
test('should return and object conforming to the expected schema', async () => {
await update(() => {
const node = $createTestElementNode();
// If you broke this test, you changed the public interface of a
// serialized Lexical Core Node. Please ensure the correct adapter
// logic is in place in the corresponding importJSON method
// to accomodate these changes.
expect(node.exportJSON()).toStrictEqual({
children: [],
direction: null,
format: '',
indent: 0,
type: 'element',
version: 1,
});
});
});
});
describe('getChildren()', () => {
test('no children', async () => {
await update(() => {

View File

@ -25,6 +25,21 @@ describe('LexicalLineBreakNode tests', () => {
});
});
test('LineBreakNode.exportJSON() should return and object conforming to the expected schema', async () => {
const {editor} = testEnv;
await editor.update(() => {
const node = $createLineBreakNode();
// If you broke this test, you changed the public interface of a
// serialized Lexical Core Node. Please ensure the correct adapter
// logic is in place in the corresponding importJSON method
// to accomodate these changes.
expect(node.exportJSON()).toStrictEqual({
type: 'linebreak',
version: 1,
});
});
});
test('LineBreakNode.createDOM()', async () => {
const {editor} = testEnv;
await editor.update(() => {

View File

@ -13,7 +13,7 @@ import {
ParagraphNode,
} from 'lexical';
import {initializeUnitTest} from '../../__tests__/utils';
import {initializeUnitTest} from '../../../__tests__/utils';
const editorConfig = Object.freeze({
theme: {
@ -37,6 +37,25 @@ describe('LexicalParagraphNode tests', () => {
expect(() => new ParagraphNode()).toThrow();
});
test('ParagraphNode.exportJSON() should return and object conforming to the expected schema', async () => {
const {editor} = testEnv;
await editor.update(() => {
const node = $createParagraphNode();
// If you broke this test, you changed the public interface of a
// serialized Lexical Core Node. Please ensure the correct adapter
// logic is in place in the corresponding importJSON method
// to accomodate these changes.
expect(node.exportJSON()).toStrictEqual({
children: [],
direction: null,
format: '',
indent: 0,
type: 'paragraph',
version: 1,
});
});
});
test('ParagraphNode.createDOM()', async () => {
const {editor} = testEnv;
await editor.update(() => {

View File

@ -58,6 +58,25 @@ describe('LexicalRootNode tests', () => {
});
});
test('RootNode.exportJSON() should return and object conforming to the expected schema', async () => {
const {editor} = testEnv;
await editor.update(() => {
const node = $createRootNode();
// If you broke this test, you changed the public interface of a
// serialized Lexical Core Node. Please ensure the correct adapter
// logic is in place in the corresponding importJSON method
// to accomodate these changes.
expect(node.exportJSON()).toStrictEqual({
children: [],
direction: null,
format: '',
indent: 0,
type: 'root',
version: 1,
});
});
});
test('RootNode.clone()', async () => {
const rootNodeClone = rootNode.constructor.clone();
expect(rootNodeClone).not.toBe(rootNode);

View File

@ -108,6 +108,27 @@ describe('LexicalTextNode tests', () => {
});
}
describe('exportJSON()', () => {
test('should return and object conforming to the expected schema', async () => {
await update(() => {
const node = $createTextNode();
// If you broke this test, you changed the public interface of a
// serialized Lexical Core Node. Please ensure the correct adapter
// logic is in place in the corresponding importJSON method
// to accomodate these changes.
expect(node.exportJSON()).toStrictEqual({
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: '',
type: 'text',
version: 1,
});
});
});
});
describe('root.getTextContent()', () => {
test('writable nodes', async () => {
let nodeKey;

View File

@ -191,7 +191,7 @@ async function build(name, inputFile, outputFile, isProd) {
{
transform(source) {
// eslint-disable-next-line no-unused-expressions
extractCodes && findAndRecordErrorCodes(source);
extractCodes && findAndRecordErrorCodes(source, isTypeScript);
return source;
},
},

View File

@ -68,5 +68,11 @@
"66": "createNode: node does not exist in nodeMap",
"67": "reconcileNode: prevNode or nextNode does not exist in nodeMap",
"68": "reconcileNode: parentDOM is null",
"69": "Reconciliation: could not find DOM element for node key \"${key}\""
"69": "Reconciliation: could not find DOM element for node key \"${key}\"",
"70": "Incorrect node type received in importJSON for %s",
"71": "LexicalNode: Node %s does not implement .exportJSON().",
"72": "LexicalNode: Node %s is an element but .exportJSON() does not have a children array.",
"73": "parseEditorState: type \"%s\" + not found",
"74": "LexicalNode: Node %s does not implement .importJSON().",
"75": "exportJSON: base method not extended"
}

View File

@ -13,19 +13,19 @@ const traverse = require('@babel/traverse').default;
const evalToString = require('./evalToString');
const invertObject = require('./invertObject');
const plugins = [
'classProperties',
'jsx',
'trailingFunctionCommas',
'objectRestSpread',
];
const babylonOptions = {
// As a parser, babylon has its own options and we can't directly
// import/require a babel preset. It should be kept **the same** as
// the `babel-plugin-syntax-*` ones specified in
// https://github.com/facebook/fbjs/blob/master/packages/babel-preset-fbjs/configure.js
plugins: [
'classProperties',
'flow',
'jsx',
'trailingFunctionCommas',
'objectRestSpread',
],
plugins,
sourceType: 'module',
};
@ -99,7 +99,13 @@ module.exports = function (opts) {
);
}
return function extractErrors(source) {
return function extractErrors(source, isTypeScript) {
if (isTypeScript) {
babylonOptions.plugins = [...plugins, 'typescript'];
} else {
babylonOptions.plugins = [...plugins, 'flow'];
}
transform(source);
flush();
};