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; const content: any;
export default content; 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, RangeSelection,
EditorThemeClasses, EditorThemeClasses,
LexicalEditor, LexicalEditor,
SerializedElementNode,
SerializedTextNode,
} from 'lexical'; } from 'lexical';
import {ElementNode, TextNode} from 'lexical'; import {ElementNode, TextNode} from 'lexical';
import {Spread} from 'libdefs/globals';
declare class CodeNode extends ElementNode { declare class CodeNode extends ElementNode {
static getType(): string; static getType(): string;
@ -31,6 +34,8 @@ declare class CodeNode extends ElementNode {
collapseAtStart(): true; collapseAtStart(): true;
setLanguage(language: string): void; setLanguage(language: string): void;
getLanguage(): string | void; getLanguage(): string | void;
importJSON(serializedNode: SerializedCodeNode): CodeNode;
exportJSON(): SerializedElementNode;
} }
declare function $createCodeNode(language?: string): CodeNode; declare function $createCodeNode(language?: string): CodeNode;
declare function $isCodeNode( declare function $isCodeNode(
@ -74,3 +79,21 @@ declare function $isCodeHighlightNode(
): node is CodeHighlightNode; ): node is CodeHighlightNode;
declare function registerCodeHighlighting(editor: LexicalEditor): () => void; 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, NodeKey,
ParagraphNode, ParagraphNode,
RangeSelection, RangeSelection,
SerializedElementNode,
SerializedTextNode,
} from 'lexical'; } from 'lexical';
import * as Prism from 'prismjs'; import * as Prism from 'prismjs';
@ -39,6 +41,7 @@ import {
mergeRegister, mergeRegister,
removeClassNamesFromElement, removeClassNamesFromElement,
} from '@lexical/utils'; } from '@lexical/utils';
import {Spread} from 'globals';
import { import {
$createLineBreakNode, $createLineBreakNode,
$createParagraphNode, $createParagraphNode,
@ -59,6 +62,24 @@ import {
const DEFAULT_CODE_LANGUAGE = 'javascript'; 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 = ( const mapToPrismLanguage = (
language: string | null | undefined, language: string | null | undefined,
): 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 { createDOM(config: EditorConfig): HTMLElement {
const element = super.createDOM(config); const element = super.createDOM(config);
const className = getHighlightThemeClass( const className = getHighlightThemeClass(
@ -134,6 +160,25 @@ export class CodeHighlightNode extends TextNode {
return update; 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) // Prevent formatting (bold, underline, etc)
setFormat(format: number): this { setFormat(format: number): this {
return 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 // Mutation
insertNewAfter( insertNewAfter(
selection: RangeSelection, 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'; import {TextNode} from 'lexical';
export declare class HashtagNode extends TextNode { export declare class HashtagNode extends TextNode {
@ -16,6 +21,7 @@ export declare class HashtagNode extends TextNode {
createDOM(config: EditorConfig): HTMLElement; createDOM(config: EditorConfig): HTMLElement;
canInsertTextBefore(): boolean; canInsertTextBefore(): boolean;
isTextEntity(): true; isTextEntity(): true;
static importJSON(serializedNode: SerializedTextNode): HashtagNode;
} }
export function $createHashtagNode(text?: string): TextNode; export function $createHashtagNode(text?: string): TextNode;
export function $isHashtagNode( export function $isHashtagNode(

View File

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

View File

@ -7,7 +7,12 @@
* @flow strict * @flow strict
*/ */
import type {EditorConfig, LexicalNode, NodeKey} from 'lexical'; import type {
EditorConfig,
LexicalNode,
NodeKey,
SerializedTextNode,
} from 'lexical';
import {addClassNamesToElement} from '@lexical/utils'; import {addClassNamesToElement} from '@lexical/utils';
import {TextNode} from 'lexical'; import {TextNode} from 'lexical';
@ -31,6 +36,22 @@ export class HashtagNode extends TextNode {
return element; 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 { canInsertTextBefore(): boolean {
return false; return false;
} }

View File

@ -15,10 +15,20 @@ import type {
LexicalNode, LexicalNode,
NodeKey, NodeKey,
RangeSelection, RangeSelection,
SerializedElementNode,
} from 'lexical'; } from 'lexical';
import {addClassNamesToElement} from '@lexical/utils'; import {addClassNamesToElement} from '@lexical/utils';
import {$isElementNode, createCommand, ElementNode} from 'lexical'; 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 { export class LinkNode extends ElementNode {
__url: string; __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 { getURL(): string {
return this.getLatest().__url; return this.getLatest().__url;
} }
@ -119,6 +145,13 @@ export function $isLinkNode(node: ?LexicalNode): boolean %checks {
return node instanceof LinkNode; return node instanceof LinkNode;
} }
export type SerializedAutoLinkNode = {
...SerializedLinkNode,
type: 'autolink',
version: 1,
...
};
// Custom node type to override `canInsertTextAfter` that will // Custom node type to override `canInsertTextAfter` that will
// allow typing within the link // allow typing within the link
export class AutoLinkNode extends LinkNode { export class AutoLinkNode extends LinkNode {
@ -131,6 +164,28 @@ export class AutoLinkNode extends LinkNode {
return new AutoLinkNode(node.__url, node.__key); 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 { insertNewAfter(selection: RangeSelection): null | ElementNode {
const element = this.getParentOrThrow().insertNewAfter(selection); const element = this.getParentOrThrow().insertNewAfter(selection);
if ($isElementNode(element)) { if ($isElementNode(element)) {

View File

@ -7,6 +7,7 @@
*/ */
import {ListNodeTagType} from './src/LexicalListNode'; import {ListNodeTagType} from './src/LexicalListNode';
import {Spread} from 'globals';
import { import {
ElementNode, ElementNode,
LexicalNode, LexicalNode,
@ -14,6 +15,7 @@ import {
ParagraphNode, ParagraphNode,
RangeSelection, RangeSelection,
LexicalCommand, LexicalCommand,
SerializedElementNode,
} from 'lexical'; } from 'lexical';
export type ListType = 'number' | 'bullet' | 'check'; export type ListType = 'number' | 'bullet' | 'check';
@ -40,13 +42,18 @@ export declare class ListItemNode extends ElementNode {
getChecked(): boolean | void; getChecked(): boolean | void;
setChecked(boolean): this; setChecked(boolean): this;
toggleChecked(): void; toggleChecked(): void;
static importJSON(serializedNode: SerializedListItemNode): ListItemNode;
exportJSON(): SerializedListItemNode;
} }
export declare class ListNode extends ElementNode { export declare class ListNode extends ElementNode {
canBeEmpty(): false; canBeEmpty(): false;
append(...nodesToAppend: LexicalNode[]): ListNode; append(...nodesToAppend: LexicalNode[]): ListNode;
getTag(): ListNodeTagType; getTag(): ListNodeTagType;
getListType(): ListType; getListType(): ListType;
static importJSON(serializedNode: SerializedListNode): ListNode;
exportJSON(): SerializedListNode;
} }
export function outdentList(): void; export function outdentList(): void;
export function removeList(editor: LexicalEditor): boolean; 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_ORDERED_LIST_COMMAND: LexicalCommand<void>;
export var INSERT_CHECK_LIST_COMMAND: LexicalCommand<void>; export var INSERT_CHECK_LIST_COMMAND: LexicalCommand<void>;
export var REMOVE_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, ParagraphNode,
RangeSelection, RangeSelection,
LexicalCommand, LexicalCommand,
SerializedElementNode,
} from 'lexical'; } from 'lexical';
import {ElementNode} from 'lexical'; import {ElementNode} from 'lexical';
@ -53,6 +54,7 @@ declare export class ListItemNode extends ElementNode {
getChecked(): boolean | void; getChecked(): boolean | void;
setChecked(boolean): this; setChecked(boolean): this;
toggleChecked(): void; toggleChecked(): void;
static importJSON(serializedNode: SerializedListItemNode): ListItemNode;
} }
declare export class ListNode extends ElementNode { declare export class ListNode extends ElementNode {
__tag: ListNodeTagType; __tag: ListNodeTagType;
@ -62,6 +64,7 @@ declare export class ListNode extends ElementNode {
getTag(): ListNodeTagType; getTag(): ListNodeTagType;
getStart(): number; getStart(): number;
getListType(): ListType; getListType(): ListType;
static importJSON(serializedNode: SerializedListNode): ListNode;
} }
declare export function outdentList(): void; declare export function outdentList(): void;
declare export function removeList(editor: LexicalEditor): boolean; 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_ORDERED_LIST_COMMAND: LexicalCommand<void>;
declare export var INSERT_CHECK_LIST_COMMAND: LexicalCommand<void>; declare export var INSERT_CHECK_LIST_COMMAND: LexicalCommand<void>;
declare export var REMOVE_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, NodeSelection,
ParagraphNode, ParagraphNode,
RangeSelection, RangeSelection,
SerializedElementNode,
} from 'lexical'; } from 'lexical';
import { import {
@ -41,6 +42,15 @@ import {
updateChildrenListItemValue, updateChildrenListItemValue,
} from './formatList'; } from './formatList';
export type SerializedListItemNode = {
...SerializedElementNode,
checked: boolean | void,
type: 'listitem',
value: number,
version: 1,
...
};
export class ListItemNode extends ElementNode { export class ListItemNode extends ElementNode {
__value: number; __value: number;
__checked: boolean | void; __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 { append(...nodes: LexicalNode[]): ListItemNode {
for (let i = 0; i < nodes.length; i++) { for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]; const node = nodes[i];
@ -262,8 +289,13 @@ export class ListItemNode extends ElementNode {
} }
getIndent(): number { 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. // ListItemNode should always have a ListNode for a parent.
let listNodeParent = this.getParentOrThrow().getParentOrThrow(); let listNodeParent = parent.getParentOrThrow();
let indentLevel = 0; let indentLevel = 0;
while ($isListItemNode(listNodeParent)) { while ($isListItemNode(listNodeParent)) {
listNodeParent = listNodeParent.getParentOrThrow().getParentOrThrow(); listNodeParent = listNodeParent.getParentOrThrow().getParentOrThrow();

View File

@ -14,6 +14,7 @@ import type {
EditorThemeClasses, EditorThemeClasses,
LexicalNode, LexicalNode,
NodeKey, NodeKey,
SerializedElementNode,
} from 'lexical'; } from 'lexical';
import { import {
@ -25,6 +26,16 @@ import {$createTextNode, ElementNode} from 'lexical';
import {$createListItemNode, $isListItemNode} from '.'; import {$createListItemNode, $isListItemNode} from '.';
import {$getListDepth} from './utils'; 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 ListType = 'number' | 'bullet' | 'check';
export type ListNodeTagType = 'ul' | 'ol'; 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 { canBeEmpty(): false {
return false; return false;
} }

View File

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

View File

@ -7,10 +7,23 @@
* @flow strict * @flow strict
*/ */
import type {EditorConfig, LexicalNode, NodeKey, RangeSelection} from 'lexical'; import type {
EditorConfig,
LexicalNode,
NodeKey,
RangeSelection,
SerializedElementNode,
} from 'lexical';
import {ElementNode} from 'lexical'; import {ElementNode} from 'lexical';
export type SerializedOverflowNode = {
...SerializedElementNode,
type: 'overflow',
version: 1,
...
};
export class OverflowNode extends ElementNode { export class OverflowNode extends ElementNode {
static getType(): string { static getType(): string {
return 'overflow'; return 'overflow';
@ -20,11 +33,22 @@ export class OverflowNode extends ElementNode {
return new OverflowNode(node.__key); return new OverflowNode(node.__key);
} }
static importJSON(serializedNode: SerializedOverflowNode): OverflowNode {
return $createOverflowNode();
}
constructor(key?: NodeKey): void { constructor(key?: NodeKey): void {
super(key); super(key);
this.__type = 'overflow'; this.__type = 'overflow';
} }
exportJSON(): SerializedElementNode {
return {
...super.exportJSON(),
type: 'overflow',
};
}
createDOM(config: EditorConfig): HTMLElement { createDOM(config: EditorConfig): HTMLElement {
const div = document.createElement('span'); const div = document.createElement('span');
const className = config.theme.characterLimit; 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'; import {TextNode} from 'lexical';
export type SerializedEmojiNode = Spread<
{
className: string;
type: 'emoji';
},
SerializedTextNode
>;
export class EmojiNode extends TextNode { export class EmojiNode extends TextNode {
__className?: string; __className: string;
static getType(): string { static getType(): string {
return 'emoji'; return 'emoji';
@ -47,6 +61,31 @@ export class EmojiNode extends TextNode {
super.updateDOM(prevNode, inner as HTMLElement, config); super.updateDOM(prevNode, inner as HTMLElement, config);
return false; 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( export function $isEmojiNode(

View File

@ -11,10 +11,12 @@ import type {
EditorConfig, EditorConfig,
LexicalNode, LexicalNode,
NodeKey, NodeKey,
SerializedLexicalNode,
} from 'lexical'; } from 'lexical';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {mergeRegister} from '@lexical/utils'; import {mergeRegister} from '@lexical/utils';
import {Spread} from 'globals';
import { import {
$getNodeByKey, $getNodeByKey,
COMMAND_PRIORITY_HIGH, 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> { export class EquationNode extends DecoratorNode<JSX.Element> {
__equation: string; __equation: string;
__inline: boolean; __inline: boolean;
@ -132,6 +143,23 @@ export class EquationNode extends DecoratorNode<JSX.Element> {
this.__inline = inline ?? false; 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 { exportDOM(): DOMExportOutput {
const element = document.createElement(this.__inline ? 'span' : 'div'); const element = document.createElement(this.__inline ? 'span' : 'div');
element.innerText = this.__equation; element.innerText = this.__equation;

View File

@ -7,11 +7,18 @@
*/ */
import type {ExcalidrawElementFragment} from './ExcalidrawModal'; 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 {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {useLexicalNodeSelection} from '@lexical/react/useLexicalNodeSelection'; import {useLexicalNodeSelection} from '@lexical/react/useLexicalNodeSelection';
import {mergeRegister} from '@lexical/utils'; import {mergeRegister} from '@lexical/utils';
import {Spread} from 'globals';
import { import {
$getNodeByKey, $getNodeByKey,
$getSelection, $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> { export class ExcalidrawNode extends DecoratorNode<JSX.Element> {
__data: string; __data: string;
@ -204,6 +220,18 @@ export class ExcalidrawNode extends DecoratorNode<JSX.Element> {
return new ExcalidrawNode(node.__data, node.__key); 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) { constructor(data = '[]', key?: NodeKey) {
super(key); super(key);
this.__data = data; this.__data = data;

View File

@ -12,6 +12,7 @@ import type {
LexicalEditor, LexicalEditor,
LexicalNode, LexicalNode,
NodeKey, NodeKey,
SerializedLexicalNode,
} from 'lexical'; } from 'lexical';
import './ImageNode.css'; import './ImageNode.css';
@ -29,6 +30,7 @@ import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {TablePlugin} from '@lexical/react/LexicalTablePlugin'; import {TablePlugin} from '@lexical/react/LexicalTablePlugin';
import {useLexicalNodeSelection} from '@lexical/react/useLexicalNodeSelection'; import {useLexicalNodeSelection} from '@lexical/react/useLexicalNodeSelection';
import {mergeRegister} from '@lexical/utils'; import {mergeRegister} from '@lexical/utils';
import {Spread} from 'globals';
import { import {
$getNodeByKey, $getNodeByKey,
$getSelection, $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> { export class ImageNode extends DecoratorNode<JSX.Element> {
__src: string; __src: string;
__altText: 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( constructor(
src: string, src: string,
altText: string, altText: string,
@ -363,6 +394,20 @@ export class ImageNode extends DecoratorNode<JSX.Element> {
return {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( setWidthAndHeight(
width: 'inherit' | number, width: 'inherit' | number,
height: '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'; import {TextNode} from 'lexical';
export type SerializedKeywordNode = Spread<
{
type: 'keyword';
version: 1;
},
SerializedTextNode
>;
export class KeywordNode extends TextNode { export class KeywordNode extends TextNode {
static getType(): string { static getType(): string {
return 'keyword'; return 'keyword';
@ -19,6 +28,23 @@ export class KeywordNode extends TextNode {
return new KeywordNode(node.__text, node.__key); 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 { createDOM(config: EditorConfig): HTMLElement {
const dom = super.createDOM(config); const dom = super.createDOM(config);
dom.style.cursor = 'default'; 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'; 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)'; const mentionStyle = 'background-color: rgba(24, 119, 232, 0.2)';
export class MentionNode extends TextNode { export class MentionNode extends TextNode {
__mention: string; __mention: string;
@ -21,12 +36,30 @@ export class MentionNode extends TextNode {
static clone(node: MentionNode): MentionNode { static clone(node: MentionNode): MentionNode {
return new MentionNode(node.__mention, node.__text, node.__key); 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) { constructor(mentionName: string, text?: string, key?: NodeKey) {
super(text ?? mentionName, key); super(text ?? mentionName, key);
this.__mention = mentionName; this.__mention = mentionName;
} }
exportJSON(): SerializedMentionNode {
return {
...super.exportJSON(),
mentionName: this.__mention,
type: 'mention',
version: 1,
};
}
createDOM(config: EditorConfig): HTMLElement { createDOM(config: EditorConfig): HTMLElement {
const dom = super.createDOM(config); const dom = super.createDOM(config);
dom.style.cssText = mentionStyle; 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 './PollNode.css';
import {useCollaborationContext} from '@lexical/react/LexicalCollaborationPlugin'; import {useCollaborationContext} from '@lexical/react/LexicalCollaborationPlugin';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {Spread} from 'globals';
import {$getNodeByKey, DecoratorNode} from 'lexical'; import {$getNodeByKey, DecoratorNode} from 'lexical';
import * as React from 'react'; import * as React from 'react';
import {useMemo, useRef} 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> { export class PollNode extends DecoratorNode<JSX.Element> {
__question: string; __question: string;
__options: Options; __options: Options;
@ -201,12 +212,27 @@ export class PollNode extends DecoratorNode<JSX.Element> {
return new PollNode(node.__question, node.__options, node.__key); 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) { constructor(question: string, options?: Options, key?: NodeKey) {
super(key); super(key);
this.__question = question; this.__question = question;
this.__options = options || [createPollOption(), createPollOption()]; this.__options = options || [createPollOption(), createPollOption()];
} }
exportJSON(): SerializedPollNode {
return {
options: this.__options,
question: this.__question,
type: 'poll',
version: 1,
};
}
addOption(option: Option): void { addOption(option: Option): void {
const self = this.getWritable<PollNode>(); const self = this.getWritable<PollNode>();
const options = Array.from(self.__options); 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'; import './StickyNode.css';
@ -18,6 +24,7 @@ import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin'; import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {LexicalNestedComposer} from '@lexical/react/LexicalNestedComposer'; import {LexicalNestedComposer} from '@lexical/react/LexicalNestedComposer';
import {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin'; import {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin';
import {Spread} from 'globals';
import { import {
$getNodeByKey, $getNodeByKey,
$setSelection, $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> { export class StickyNode extends DecoratorNode<JSX.Element> {
__x: number; __x: number;
__y: number; __y: number;
__color: 'pink' | 'yellow'; __color: StickyNoteColor;
__caption: LexicalEditor; __caption: LexicalEditor;
static getType(): string { static getType(): string {
@ -285,6 +306,14 @@ export class StickyNode extends DecoratorNode<JSX.Element> {
node.__key, node.__key,
); );
} }
static importJSON(serializedNode: SerializedStickyNode): StickyNode {
return new StickyNode(
serializedNode.xOffset,
serializedNode.yOffset,
serializedNode.color,
serializedNode.caption,
);
}
constructor( constructor(
x: number, x: number,
@ -300,6 +329,17 @@ export class StickyNode extends DecoratorNode<JSX.Element> {
this.__color = color; 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 { createDOM(config: EditorConfig): HTMLElement {
const div = document.createElement('div'); const div = document.createElement('div');
div.style.display = 'contents'; div.style.display = 'contents';

View File

@ -9,7 +9,11 @@
import type {ElementFormatType, LexicalNode, NodeKey} from 'lexical'; import type {ElementFormatType, LexicalNode, NodeKey} from 'lexical';
import {BlockWithAlignableContents} from '@lexical/react/LexicalBlockWithAlignableContents'; 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 * as React from 'react';
import {useCallback, useEffect, useRef, useState} from 'react'; import {useCallback, useEffect, useRef, useState} from 'react';
@ -42,6 +46,7 @@ function TweetComponent({
const createTweet = useCallback(async () => { const createTweet = useCallback(async () => {
try { try {
// @ts-expect-error Twitter is attached to the window.
await window.twttr.widgets.createTweet(tweetID, containerRef.current); await window.twttr.widgets.createTweet(tweetID, containerRef.current);
setIsLoading(false); 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> { export class TweetNode extends DecoratorBlockNode<JSX.Element> {
__id: string; __id: string;
@ -99,6 +113,21 @@ export class TweetNode extends DecoratorBlockNode<JSX.Element> {
return new TweetNode(node.__id, node.__format, node.__key); 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) { constructor(id: string, format?: ElementFormatType | null, key?: NodeKey) {
super(format, key); super(format, key);
this.__id = id; 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'; import {TextNode} from 'lexical';
export type SerializedTypeaheadNode = Spread<
{
type: 'typeahead';
version: 1;
},
SerializedTextNode
>;
export class TypeaheadNode extends TextNode { export class TypeaheadNode extends TextNode {
static clone(node: TypeaheadNode): TypeaheadNode { static clone(node: TypeaheadNode): TypeaheadNode {
return new TypeaheadNode(node.__text, node.__key); return new TypeaheadNode(node.__text, node.__key);
@ -19,6 +28,23 @@ export class TypeaheadNode extends TextNode {
return 'typeahead'; 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 { createDOM(config: EditorConfig): HTMLElement {
const dom = super.createDOM(config); const dom = super.createDOM(config);
dom.style.cssText = 'color: #ccc;'; dom.style.cssText = 'color: #ccc;';

View File

@ -9,7 +9,11 @@
import type {ElementFormatType, LexicalNode, NodeKey} from 'lexical'; import type {ElementFormatType, LexicalNode, NodeKey} from 'lexical';
import {BlockWithAlignableContents} from '@lexical/react/LexicalBlockWithAlignableContents'; 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 * as React from 'react';
type YouTubeComponentProps = Readonly<{ 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> { export class YouTubeNode extends DecoratorBlockNode<JSX.Element> {
__id: string; __id: string;
@ -45,6 +58,21 @@ export class YouTubeNode extends DecoratorBlockNode<JSX.Element> {
return new YouTubeNode(node.__id, node.__format, node.__key); 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) { constructor(id: string, format?: ElementFormatType | null, key?: NodeKey) {
super(format, key); super(format, key);
this.__id = id; this.__id = id;

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
* *
*/ */
import type { import type {
DOMConversionMap, DOMConversionMap,
EditorConfig, EditorConfig,
@ -13,8 +14,11 @@ import type {
NodeKey, NodeKey,
ParagraphNode, ParagraphNode,
LexicalEditor, LexicalEditor,
SerializedElementNode,
} from 'lexical'; } from 'lexical';
import {ElementNode} from 'lexical'; import {ElementNode} from 'lexical';
import {Spread} from 'libdefs/globals';
export type InitialEditorStateType = null | string | EditorState | (() => void); export type InitialEditorStateType = null | string | EditorState | (() => void);
export declare class QuoteNode extends ElementNode { export declare class QuoteNode extends ElementNode {
@ -25,6 +29,7 @@ export declare class QuoteNode extends ElementNode {
updateDOM(prevNode: QuoteNode, dom: HTMLElement): boolean; updateDOM(prevNode: QuoteNode, dom: HTMLElement): boolean;
insertNewAfter(): ParagraphNode; insertNewAfter(): ParagraphNode;
collapseAtStart(): true; collapseAtStart(): true;
importJSON(serializedNode: SerializedQuoteNode): QuoteNode;
} }
export function $createQuoteNode(): QuoteNode; export function $createQuoteNode(): QuoteNode;
export function $isQuoteNode( export function $isQuoteNode(
@ -42,7 +47,9 @@ export declare class HeadingNode extends ElementNode {
static importDOM(): DOMConversionMap | null; static importDOM(): DOMConversionMap | null;
insertNewAfter(): ParagraphNode; insertNewAfter(): ParagraphNode;
collapseAtStart(): true; collapseAtStart(): true;
importJSON(serializedNode: SerializedHeadingNode): QuoteNode;
} }
export function $createHeadingNode(headingTag: HeadingTagType): HeadingNode; export function $createHeadingNode(headingTag: HeadingTagType): HeadingNode;
export function $isHeadingNode( export function $isHeadingNode(
node: LexicalNode | null | undefined, node: LexicalNode | null | undefined,
@ -51,3 +58,20 @@ export function registerRichText(
editor: LexicalEditor, editor: LexicalEditor,
initialEditorState?: InitialEditorStateType, initialEditorState?: InitialEditorStateType,
): () => void; ): () => 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, LexicalNode,
NodeKey, NodeKey,
ParagraphNode, ParagraphNode,
SerializedElementNode,
TextFormatType, TextFormatType,
} from 'lexical'; } from 'lexical';
@ -33,6 +34,7 @@ import {
addClassNamesToElement, addClassNamesToElement,
mergeRegister, mergeRegister,
} from '@lexical/utils'; } from '@lexical/utils';
import {Spread} from 'globals';
import { import {
$createParagraphNode, $createParagraphNode,
$getRoot, $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 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. // Convoluted logic to make this work with Flow. Order matters.
const options = {tag: 'history-merge'}; const options = {tag: 'history-merge'};
const setEditorOptions: { const setEditorOptions: {
@ -107,6 +126,21 @@ export class QuoteNode extends ElementNode {
return false; 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 // Mutation
insertNewAfter(): ParagraphNode { 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 // Mutation
insertNewAfter(): ParagraphNode { insertNewAfter(): ParagraphNode {

View File

@ -15,6 +15,8 @@ import type {
LexicalEditor, LexicalEditor,
LexicalNode, LexicalNode,
NodeKey, NodeKey,
SerializedElementNode,
SerializedGridCellNode,
} from 'lexical'; } from 'lexical';
import {addClassNamesToElement} from '@lexical/utils'; import {addClassNamesToElement} from '@lexical/utils';
@ -34,6 +36,14 @@ export const TableCellHeaderStates = {
export type TableCellHeaderState = $Values<typeof TableCellHeaderStates>; export type TableCellHeaderState = $Values<typeof TableCellHeaderStates>;
export type SerializedTableCellNode = {
...SerializedGridCellNode,
headerState: TableCellHeaderState,
type: 'tablecell',
width: number,
...
};
export class TableCellNode extends GridCellNode { export class TableCellNode extends GridCellNode {
__headerState: TableCellHeaderState; __headerState: TableCellHeaderState;
__width: ?number; __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( constructor(
headerState?: TableCellHeaderState = TableCellHeaderStates.NO_STATUS, headerState?: TableCellHeaderState = TableCellHeaderStates.NO_STATUS,
colSpan?: number = 1, 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 { getTag(): string {
return this.hasHeader() ? 'th' : 'td'; return this.hasHeader() ? 'th' : 'td';
} }

View File

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

View File

@ -13,11 +13,20 @@ import type {
EditorConfig, EditorConfig,
LexicalNode, LexicalNode,
NodeKey, NodeKey,
SerializedElementNode,
} from 'lexical'; } from 'lexical';
import {addClassNamesToElement} from '@lexical/utils'; import {addClassNamesToElement} from '@lexical/utils';
import {GridRowNode} from 'lexical'; import {GridRowNode} from 'lexical';
export type SerializedTableRowNode = {
...SerializedElementNode,
height: number,
type: 'tablerow',
version: 1,
...
};
export class TableRowNode extends GridRowNode { export class TableRowNode extends GridRowNode {
__height: ?number; __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 { constructor(height?: ?number, key?: NodeKey): void {
super(key); super(key);
this.__height = height; this.__height = height;
} }
exportJSON(): SerializedElementNode {
return {
...super.exportJSON(),
type: 'tablerow',
version: 1,
};
}
createDOM(config: EditorConfig): HTMLElement { createDOM(config: EditorConfig): HTMLElement {
const element = document.createElement('tr'); const element = document.createElement('tr');

View File

@ -7,6 +7,7 @@
*/ */
import {Class, $ReadOnly} from 'utility-types'; import {Class, $ReadOnly} from 'utility-types';
import {Spread} from 'globals';
/** /**
* LexicalCommands * LexicalCommands
@ -161,6 +162,10 @@ export declare class LexicalEditor {
parseEditorState( parseEditorState(
maybeStringifiedEditorState: string | ParsedEditorState, maybeStringifiedEditorState: string | ParsedEditorState,
): EditorState; ): EditorState;
unstable_parseEditorState(
maybeStringifiedEditorState: string | SerializedEditorState,
updateFn?: () => void,
): EditorState;
update(updateFn: () => void, options?: EditorUpdateOptions): boolean; update(updateFn: () => void, options?: EditorUpdateOptions): boolean;
focus(callbackFn?: () => void): void; focus(callbackFn?: () => void): void;
blur(): void; blur(): void;
@ -291,6 +296,7 @@ export interface EditorState {
isEmpty(): boolean; isEmpty(): boolean;
read<V>(callbackFn: () => V): V; read<V>(callbackFn: () => V): V;
toJSON(space?: string | number): JSONEditorState; toJSON(space?: string | number): JSONEditorState;
unstable_toJSON(): SerializedEditorState;
clone( clone(
selection?: RangeSelection | NodeSelection | GridSelection | null, selection?: RangeSelection | NodeSelection | GridSelection | null,
): EditorState; ): EditorState;
@ -605,7 +611,10 @@ export declare class TextNode extends LexicalNode {
toggleFormat(type: TextFormatType): TextNode; toggleFormat(type: TextFormatType): TextNode;
toggleDirectionless(): TextNode; toggleDirectionless(): TextNode;
toggleUnmergeable(): TextNode; toggleUnmergeable(): TextNode;
setMode(type: TextModeType): TextNode; setMode(type: TextModeType): this;
setDetail(detail: number): TextNode;
getDetail(): number;
getMode(): TextModeType;
setTextContent(text: string): TextNode; setTextContent(text: string): TextNode;
select(_anchorOffset?: number, _focusOffset?: number): RangeSelection; select(_anchorOffset?: number, _focusOffset?: number): RangeSelection;
spliceText( spliceText(
@ -619,6 +628,8 @@ export declare class TextNode extends LexicalNode {
splitText(...splitOffsets: Array<number>): Array<TextNode>; splitText(...splitOffsets: Array<number>): Array<TextNode>;
mergeWithSibling(target: TextNode): TextNode; mergeWithSibling(target: TextNode): TextNode;
isTextEntity(): boolean; isTextEntity(): boolean;
static importJSON(serializedTextNode: SerializedTextNode): TextNode;
exportJSON(): SerializedTextNode;
} }
export function $createTextNode(text?: string): TextNode; export function $createTextNode(text?: string): TextNode;
export function $isTextNode( export function $isTextNode(
@ -635,6 +646,10 @@ export declare class LineBreakNode extends LexicalNode {
getTextContent(): '\n'; getTextContent(): '\n';
createDOM(): HTMLElement; createDOM(): HTMLElement;
updateDOM(): false; updateDOM(): false;
static importJSON(
serializedLineBreakNode: SerializedLexicalNode,
): LineBreakNode;
exportJSON(): SerializedLexicalNode;
} }
export function $createLineBreakNode(): LineBreakNode; export function $createLineBreakNode(): LineBreakNode;
export function $isLineBreakNode( export function $isLineBreakNode(
@ -658,6 +673,8 @@ export declare class RootNode extends ElementNode {
updateDOM(prevNode: RootNode, dom: HTMLElement): false; updateDOM(prevNode: RootNode, dom: HTMLElement): false;
append(...nodesToAppend: Array<LexicalNode>): ElementNode; append(...nodesToAppend: Array<LexicalNode>): ElementNode;
canBeEmpty(): false; canBeEmpty(): false;
static importJSON(serializedRootNode: SerializedRootNode): RootNode;
exportJSON(): SerializedElementNode;
} }
export function $isRootNode( export function $isRootNode(
node: LexicalNode | null | undefined, node: LexicalNode | null | undefined,
@ -674,6 +691,7 @@ export declare class ElementNode extends LexicalNode {
__dir: 'ltr' | 'rtl' | null; __dir: 'ltr' | 'rtl' | null;
constructor(key?: NodeKey); constructor(key?: NodeKey);
getFormat(): number; getFormat(): number;
getFormatType(): 'left' | 'center' | 'right' | 'justify';
getIndent(): number; getIndent(): number;
getChildren<T extends Array<LexicalNode>>(): T; getChildren<T extends Array<LexicalNode>>(): T;
getChildrenKeys(): Array<NodeKey>; getChildrenKeys(): Array<NodeKey>;
@ -722,6 +740,7 @@ export declare class ElementNode extends LexicalNode {
deleteCount: number, deleteCount: number,
nodesToInsert: Array<LexicalNode>, nodesToInsert: Array<LexicalNode>,
): ElementNode; ): ElementNode;
exportJSON(): SerializedElementNode;
} }
export function $isElementNode( export function $isElementNode(
node: LexicalNode | null | undefined, node: LexicalNode | null | undefined,
@ -751,6 +770,10 @@ export declare class ParagraphNode extends ElementNode {
updateDOM(prevNode: ParagraphNode, dom: HTMLElement): boolean; updateDOM(prevNode: ParagraphNode, dom: HTMLElement): boolean;
insertNewAfter(): ParagraphNode; insertNewAfter(): ParagraphNode;
collapseAtStart(): boolean; collapseAtStart(): boolean;
static importJSON(
serializedParagraphNode: SerializedElementNode,
): ParagraphNode;
exportJSON(): SerializedElementNode;
} }
export function $createParagraphNode(): ParagraphNode; export function $createParagraphNode(): ParagraphNode;
export function $isParagraphNode( export function $isParagraphNode(
@ -797,3 +820,49 @@ export function $getDecoratorNode(
* LexicalVersion * LexicalVersion
*/ */
export declare var VERSION: string; 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( parseEditorState(
maybeStringifiedEditorState: string | ParsedEditorState, maybeStringifiedEditorState: string | ParsedEditorState,
): EditorState; ): EditorState;
unstable_parseEditorState<SerializedNode>(
maybeStringifiedEditorState: string | SerializedEditorState<SerializedNode>,
updateFn?: () => void,
): EditorState;
update(updateFn: () => void, options?: EditorUpdateOptions): boolean; update(updateFn: () => void, options?: EditorUpdateOptions): boolean;
focus(callbackFn?: () => void): void; focus(callbackFn?: () => void): void;
blur(): void; blur(): void;
@ -292,6 +296,7 @@ export interface EditorState {
isEmpty(): boolean; isEmpty(): boolean;
read<V>(callbackFn: () => V): V; read<V>(callbackFn: () => V): V;
toJSON(space?: string | number): JSONEditorState; toJSON(space?: string | number): JSONEditorState;
unstable_toJSON<SerializedNode>(): SerializedEditorState<SerializedNode>;
clone( clone(
selection?: RangeSelection | NodeSelection | GridSelection | null, selection?: RangeSelection | NodeSelection | GridSelection | null,
): EditorState; ): EditorState;
@ -631,6 +636,9 @@ declare export class TextNode extends LexicalNode {
toggleDirectionless(): this; toggleDirectionless(): this;
toggleUnmergeable(): this; toggleUnmergeable(): this;
setMode(type: TextModeType): this; setMode(type: TextModeType): this;
setDetail(detail: number): this;
getDetail(): number;
getMode(): TextModeType;
setTextContent(text: string): TextNode; setTextContent(text: string): TextNode;
select(_anchorOffset?: number, _focusOffset?: number): RangeSelection; select(_anchorOffset?: number, _focusOffset?: number): RangeSelection;
spliceText( spliceText(
@ -644,6 +652,8 @@ declare export class TextNode extends LexicalNode {
splitText(...splitOffsets: Array<number>): Array<TextNode>; splitText(...splitOffsets: Array<number>): Array<TextNode>;
mergeWithSibling(target: TextNode): TextNode; mergeWithSibling(target: TextNode): TextNode;
isTextEntity(): boolean; isTextEntity(): boolean;
static importJSON(serializedTextNode: SerializedTextNode): TextNode;
exportJSON(): SerializedTextNode;
} }
declare export function $createTextNode(text?: string): TextNode; declare export function $createTextNode(text?: string): TextNode;
declare export function $isTextNode( declare export function $isTextNode(
@ -661,6 +671,10 @@ declare export class LineBreakNode extends LexicalNode {
getTextContent(): '\n'; getTextContent(): '\n';
createDOM(): HTMLElement; createDOM(): HTMLElement;
updateDOM(): false; updateDOM(): false;
static importJSON(
serializedLineBreakNode: SerializedLineBreakNode,
): LineBreakNode;
exportJSON(): SerializedLexicalNode;
} }
declare export function $createLineBreakNode(): LineBreakNode; declare export function $createLineBreakNode(): LineBreakNode;
declare export function $isLineBreakNode( declare export function $isLineBreakNode(
@ -693,7 +707,7 @@ declare export function $isRootNode(
/** /**
* LexicalElementNode * LexicalElementNode
*/ */
export type ElementFormatType = 'left' | 'center' | 'right' | 'justify'; export type ElementFormatType = 'left' | 'center' | 'right' | 'justify' | '';
declare export class ElementNode extends LexicalNode { declare export class ElementNode extends LexicalNode {
__children: Array<NodeKey>; __children: Array<NodeKey>;
__format: number; __format: number;
@ -701,6 +715,7 @@ declare export class ElementNode extends LexicalNode {
__dir: 'ltr' | 'rtl' | null; __dir: 'ltr' | 'rtl' | null;
constructor(key?: NodeKey): void; constructor(key?: NodeKey): void;
getFormat(): number; getFormat(): number;
getFormatType(): ElementFormatType;
getIndent(): number; getIndent(): number;
getChildren<T: Array<LexicalNode>>(): T; getChildren<T: Array<LexicalNode>>(): T;
getChildrenKeys(): Array<NodeKey>; getChildrenKeys(): Array<NodeKey>;
@ -749,6 +764,7 @@ declare export class ElementNode extends LexicalNode {
deleteCount: number, deleteCount: number,
nodesToInsert: Array<LexicalNode>, nodesToInsert: Array<LexicalNode>,
): ElementNode; ): ElementNode;
exportJSON(): SerializedElementNode;
} }
declare export function $isElementNode( declare export function $isElementNode(
node: ?LexicalNode, node: ?LexicalNode,
@ -779,6 +795,10 @@ declare export class ParagraphNode extends ElementNode {
updateDOM(prevNode: ParagraphNode, dom: HTMLElement): boolean; updateDOM(prevNode: ParagraphNode, dom: HTMLElement): boolean;
insertNewAfter(): ParagraphNode; insertNewAfter(): ParagraphNode;
collapseAtStart(): boolean; collapseAtStart(): boolean;
static importJSON(
serializedParagraphNode: SerializedParagraphNode,
): ParagraphNode;
exportJSON(): SerializedElementNode;
} }
declare export function $createParagraphNode(): ParagraphNode; declare export function $createParagraphNode(): ParagraphNode;
declare export function $isParagraphNode( declare export function $isParagraphNode(
@ -843,3 +863,58 @@ export type EventHandler = (
* LexicalVersion * LexicalVersion
*/ */
declare export var VERSION: string; 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, 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} = { export const TEXT_MODE_TO_TYPE: {[TextModeType]: 0 | 1 | 2 | 3} = {
inert: IS_INERT, inert: IS_INERT,
normal: IS_NORMAL, normal: IS_NORMAL,
segmented: IS_SEGMENTED, segmented: IS_SEGMENTED,
token: IS_TOKEN, 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 * @flow strict
*/ */
import type {EditorState, ParsedEditorState} from './LexicalEditorState'; import type {
EditorState,
ParsedEditorState,
SerializedEditorState,
} from './LexicalEditorState';
import type {DOMConversion, LexicalNode, NodeKey} from './LexicalNode'; import type {DOMConversion, LexicalNode, NodeKey} from './LexicalNode';
import getDOMSelection from 'shared/getDOMSelection'; import getDOMSelection from 'shared/getDOMSelection';
@ -22,6 +26,7 @@ import {
commitPendingUpdates, commitPendingUpdates,
parseEditorState, parseEditorState,
triggerListeners, triggerListeners,
unstable_parseEditorState,
updateEditor, updateEditor,
} from './LexicalUpdates'; } from './LexicalUpdates';
import { import {
@ -575,6 +580,7 @@ export class LexicalEditor {
} }
commitPendingUpdates(this); commitPendingUpdates(this);
} }
// TODO: once unstable_parseEditorState is stable, swap that for this.
parseEditorState( parseEditorState(
maybeStringifiedEditorState: string | ParsedEditorState, maybeStringifiedEditorState: string | ParsedEditorState,
): EditorState { ): EditorState {
@ -584,6 +590,16 @@ export class LexicalEditor {
: maybeStringifiedEditorState; : maybeStringifiedEditorState;
return parseEditorState(parsedEditorState, this); 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 { update(updateFn: () => void, options?: EditorUpdateOptions): void {
updateEditor(this, updateFn, options); updateEditor(this, updateFn, options);
} }

View File

@ -15,25 +15,34 @@ import type {
NodeSelection, NodeSelection,
RangeSelection, RangeSelection,
} from './LexicalSelection'; } from './LexicalSelection';
import type {SerializedRootNode} from './nodes/LexicalRootNode';
import invariant from '../../shared/src/invariant';
import {$isElementNode} from '.';
import { import {
$isGridSelection, $isGridSelection,
$isNodeSelection, $isNodeSelection,
$isRangeSelection, $isRangeSelection,
} from './LexicalSelection'; } from './LexicalSelection';
import {readEditorState} from './LexicalUpdates'; import {readEditorState} from './LexicalUpdates';
import {$getRoot} from './LexicalUtils';
import {$createRootNode} from './nodes/LexicalRootNode'; import {$createRootNode} from './nodes/LexicalRootNode';
// TODO: deprecated
export type ParsedEditorState = { export type ParsedEditorState = {
_nodeMap: Array<[NodeKey, ParsedNode]>, _nodeMap: Array<[NodeKey, ParsedNode]>,
_selection: null | ParsedSelection, _selection: null | ParsedSelection,
}; };
// TODO: deprecated
export type JSONEditorState = { export type JSONEditorState = {
_nodeMap: Array<[NodeKey, LexicalNode]>, _nodeMap: Array<[NodeKey, LexicalNode]>,
_selection: null | ParsedSelection, _selection: null | ParsedSelection,
}; };
export interface SerializedEditorState {
root: SerializedRootNode;
}
export function editorStateHasDirtySelection( export function editorStateHasDirtySelection(
editorState: EditorState, editorState: EditorState,
editor: LexicalEditor, editor: LexicalEditor,
@ -59,6 +68,35 @@ export function createEmptyEditorState(): EditorState {
return new EditorState(new Map([['root', $createRootNode()]])); 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 { export class EditorState {
_nodeMap: NodeMap; _nodeMap: NodeMap;
_selection: null | RangeSelection | NodeSelection | GridSelection; _selection: null | RangeSelection | NodeSelection | GridSelection;
@ -90,6 +128,7 @@ export class EditorState {
editorState._readOnly = true; editorState._readOnly = true;
return editorState; return editorState;
} }
// TODO: remove when we use the other toJSON
toJSON(space?: string | number): JSONEditorState { toJSON(space?: string | number): JSONEditorState {
const selection = this._selection; const selection = this._selection;
@ -132,4 +171,9 @@ export class EditorState {
: null, : 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 NodeMap = Map<NodeKey, LexicalNode>;
export type SerializedLexicalNode = {
type: string,
version: number,
...
};
export function removeNode( export function removeNode(
nodeToRemove: LexicalNode, nodeToRemove: LexicalNode,
restoreSelection: boolean, restoreSelection: boolean,
@ -600,6 +606,22 @@ export class LexicalNode {
return null; 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 // Setters and mutators
remove(preserveEmptyParent?: boolean): void { remove(preserveEmptyParent?: boolean): void {

View File

@ -12,19 +12,24 @@ import type {
LexicalCommand, LexicalCommand,
LexicalEditor, LexicalEditor,
MutatedNodes, MutatedNodes,
RegisteredNodes,
Transform, Transform,
} from './LexicalEditor'; } from './LexicalEditor';
import type {ParsedEditorState} from './LexicalEditorState'; import type {
ParsedEditorState,
SerializedEditorState,
} from './LexicalEditorState';
import type {LexicalNode} from './LexicalNode'; import type {LexicalNode} from './LexicalNode';
import type {NodeParserState, ParsedNode} from './LexicalParsing'; import type {NodeParserState, ParsedNode} from './LexicalParsing';
import invariant from 'shared/invariant'; import invariant from 'shared/invariant';
import {$isTextNode} from '.'; import {$isElementNode, $isTextNode} from '.';
import {FULL_RECONCILE, NO_DIRTY_NODES} from './LexicalConstants'; import {FULL_RECONCILE, NO_DIRTY_NODES} from './LexicalConstants';
import {resetEditor} from './LexicalEditor'; import {resetEditor} from './LexicalEditor';
import { import {
cloneEditorState, cloneEditorState,
createEmptyEditorState,
EditorState, EditorState,
editorStateHasDirtySelection, editorStateHasDirtySelection,
} from './LexicalEditorState'; } from './LexicalEditorState';
@ -242,6 +247,7 @@ function $applyAllTransforms(
editor._dirtyElements = dirtyElements; editor._dirtyElements = dirtyElements;
} }
// TODO: once unstable_parseEditorState is stable, swap that for this.
export function parseEditorState( export function parseEditorState(
parsedEditorState: ParsedEditorState, parsedEditorState: ParsedEditorState,
editor: LexicalEditor, editor: LexicalEditor,
@ -279,6 +285,77 @@ export function parseEditorState(
return editorState; 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 // This technically isn't an update but given we need
// exposure to the module's active bindings, we have this // exposure to the module's active bindings, we have this
// function here // 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', () => { describe('range selection', () => {
beforeEach(async () => { beforeEach(async () => {
init(); init();

View File

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

View File

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

View File

@ -11,10 +11,17 @@ import type {
DOMConversionMap, DOMConversionMap,
DOMConversionOutput, DOMConversionOutput,
NodeKey, NodeKey,
SerializedLexicalNode,
} from '../LexicalNode'; } from '../LexicalNode';
import {LexicalNode} from '../LexicalNode'; import {LexicalNode} from '../LexicalNode';
export type SerializedLineBreakNode = {
...SerializedLexicalNode,
type: 'linebreak',
...
};
export class LineBreakNode extends LexicalNode { export class LineBreakNode extends LexicalNode {
static getType(): string { static getType(): string {
return 'linebreak'; 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 { function convertLineBreakElement(node: Node): DOMConversionOutput {

View File

@ -17,11 +17,19 @@ import type {
DOMExportOutput, DOMExportOutput,
LexicalNode, LexicalNode,
} from '../LexicalNode'; } from '../LexicalNode';
import type {SerializedElementNode} from './LexicalElementNode';
import {getCachedClassNameArray} from '../LexicalUtils'; import {getCachedClassNameArray} from '../LexicalUtils';
import {ElementNode} from './LexicalElementNode'; import {ElementNode} from './LexicalElementNode';
import {$isTextNode} from './LexicalTextNode'; import {$isTextNode} from './LexicalTextNode';
export type SerializedParagraphNode = {
...SerializedElementNode,
type: 'paragraph',
version: 1,
...
};
export class ParagraphNode extends ElementNode { export class ParagraphNode extends ElementNode {
static getType(): string { static getType(): string {
return 'paragraph'; 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 // Mutation
insertNewAfter(): ParagraphNode { insertNewAfter(): ParagraphNode {

View File

@ -9,14 +9,21 @@
import type {LexicalNode} from '../LexicalNode'; import type {LexicalNode} from '../LexicalNode';
import type {ParsedElementNode} from '../LexicalParsing'; import type {ParsedElementNode} from '../LexicalParsing';
import type {SerializedElementNode} from './LexicalElementNode';
import invariant from 'shared/invariant'; import invariant from 'shared/invariant';
import {NO_DIRTY_NODES} from '../LexicalConstants'; import {NO_DIRTY_NODES} from '../LexicalConstants';
import {getActiveEditor, isCurrentlyReadOnlyMode} from '../LexicalUpdates'; import {getActiveEditor, isCurrentlyReadOnlyMode} from '../LexicalUpdates';
import {$getRoot} from '../LexicalUtils';
import {$isDecoratorNode} from './LexicalDecoratorNode'; import {$isDecoratorNode} from './LexicalDecoratorNode';
import {$isElementNode, ElementNode} from './LexicalElementNode'; import {$isElementNode, ElementNode} from './LexicalElementNode';
export type SerializedRootNode = {
...SerializedElementNode,
...
};
export class RootNode extends ElementNode { export class RootNode extends ElementNode {
__cachedText: null | string; __cachedText: null | string;
@ -93,6 +100,26 @@ export class RootNode extends ElementNode {
return super.append(...nodesToAppend); 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 { toJSON(): ParsedElementNode {
return { return {
__children: this.__children, __children: this.__children,

View File

@ -12,6 +12,7 @@ import type {
DOMConversionMap, DOMConversionMap,
DOMConversionOutput, DOMConversionOutput,
NodeKey, NodeKey,
SerializedLexicalNode,
} from '../LexicalNode'; } from '../LexicalNode';
import type { import type {
GridSelection, GridSelection,
@ -37,6 +38,7 @@ import {
IS_UNMERGEABLE, IS_UNMERGEABLE,
TEXT_MODE_TO_TYPE, TEXT_MODE_TO_TYPE,
TEXT_TYPE_TO_FORMAT, TEXT_TYPE_TO_FORMAT,
TEXT_TYPE_TO_MODE,
} from '../LexicalConstants'; } from '../LexicalConstants';
import {LexicalNode} from '../LexicalNode'; import {LexicalNode} from '../LexicalNode';
import { import {
@ -55,6 +57,16 @@ import {
toggleTextFormatType, toggleTextFormatType,
} from '../LexicalUtils'; } from '../LexicalUtils';
export type SerializedTextNode = {
...SerializedLexicalNode,
detail: number,
format: number,
mode: TextModeType,
style: string,
text: string,
...
};
export type TextFormatType = export type TextFormatType =
| 'bold' | 'bold'
| 'underline' | 'underline'
@ -264,6 +276,16 @@ export class TextNode extends LexicalNode {
return self.__format; 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 { getStyle(): string {
const self = this.getLatest(); const self = this.getLatest();
return self.__style; 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 // Mutators
selectionTransform( selectionTransform(
prevSelection: null | RangeSelection | NodeSelection | GridSelection, prevSelection: null | RangeSelection | NodeSelection | GridSelection,
@ -460,6 +503,13 @@ export class TextNode extends LexicalNode {
return self; return self;
} }
setDetail(detail: number): this {
errorOnReadOnly();
const self = this.getWritable();
self.__detail = detail;
return self;
}
setStyle(style: string): this { setStyle(style: string): this {
errorOnReadOnly(); errorOnReadOnly();
const self = this.getWritable(); 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()', () => { describe('getChildren()', () => {
test('no children', async () => { test('no children', async () => {
await update(() => { 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 () => { test('LineBreakNode.createDOM()', async () => {
const {editor} = testEnv; const {editor} = testEnv;
await editor.update(() => { await editor.update(() => {

View File

@ -13,7 +13,7 @@ import {
ParagraphNode, ParagraphNode,
} from 'lexical'; } from 'lexical';
import {initializeUnitTest} from '../../__tests__/utils'; import {initializeUnitTest} from '../../../__tests__/utils';
const editorConfig = Object.freeze({ const editorConfig = Object.freeze({
theme: { theme: {
@ -37,6 +37,25 @@ describe('LexicalParagraphNode tests', () => {
expect(() => new ParagraphNode()).toThrow(); 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 () => { test('ParagraphNode.createDOM()', async () => {
const {editor} = testEnv; const {editor} = testEnv;
await editor.update(() => { 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 () => { test('RootNode.clone()', async () => {
const rootNodeClone = rootNode.constructor.clone(); const rootNodeClone = rootNode.constructor.clone();
expect(rootNodeClone).not.toBe(rootNode); 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()', () => { describe('root.getTextContent()', () => {
test('writable nodes', async () => { test('writable nodes', async () => {
let nodeKey; let nodeKey;

View File

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

View File

@ -68,5 +68,11 @@
"66": "createNode: node does not exist in nodeMap", "66": "createNode: node does not exist in nodeMap",
"67": "reconcileNode: prevNode or nextNode does not exist in nodeMap", "67": "reconcileNode: prevNode or nextNode does not exist in nodeMap",
"68": "reconcileNode: parentDOM is null", "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 evalToString = require('./evalToString');
const invertObject = require('./invertObject'); const invertObject = require('./invertObject');
const plugins = [
'classProperties',
'jsx',
'trailingFunctionCommas',
'objectRestSpread',
];
const babylonOptions = { const babylonOptions = {
// As a parser, babylon has its own options and we can't directly // 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 // import/require a babel preset. It should be kept **the same** as
// the `babel-plugin-syntax-*` ones specified in // the `babel-plugin-syntax-*` ones specified in
// https://github.com/facebook/fbjs/blob/master/packages/babel-preset-fbjs/configure.js // https://github.com/facebook/fbjs/blob/master/packages/babel-preset-fbjs/configure.js
plugins: [ plugins,
'classProperties',
'flow',
'jsx',
'trailingFunctionCommas',
'objectRestSpread',
],
sourceType: 'module', 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); transform(source);
flush(); flush();
}; };