mirror of
https://github.com/facebook/lexical.git
synced 2025-05-17 23:26:16 +08:00
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:
2
libdefs/globals.d.ts
vendored
2
libdefs/globals.d.ts
vendored
@ -10,3 +10,5 @@ declare module '*.jpg' {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
|
||||
export type Spread<T1, T2> = {[K in Exclude<keyof T1, keyof T2>]: T1[K]} & T2;
|
||||
|
23
packages/lexical-code/LexicalCode.d.ts
vendored
23
packages/lexical-code/LexicalCode.d.ts
vendored
@ -14,9 +14,12 @@ import type {
|
||||
RangeSelection,
|
||||
EditorThemeClasses,
|
||||
LexicalEditor,
|
||||
SerializedElementNode,
|
||||
SerializedTextNode,
|
||||
} from 'lexical';
|
||||
|
||||
import {ElementNode, TextNode} from 'lexical';
|
||||
import {Spread} from 'libdefs/globals';
|
||||
|
||||
declare class CodeNode extends ElementNode {
|
||||
static getType(): string;
|
||||
@ -31,6 +34,8 @@ declare class CodeNode extends ElementNode {
|
||||
collapseAtStart(): true;
|
||||
setLanguage(language: string): void;
|
||||
getLanguage(): string | void;
|
||||
importJSON(serializedNode: SerializedCodeNode): CodeNode;
|
||||
exportJSON(): SerializedElementNode;
|
||||
}
|
||||
declare function $createCodeNode(language?: string): CodeNode;
|
||||
declare function $isCodeNode(
|
||||
@ -74,3 +79,21 @@ declare function $isCodeHighlightNode(
|
||||
): node is CodeHighlightNode;
|
||||
|
||||
declare function registerCodeHighlighting(editor: LexicalEditor): () => void;
|
||||
|
||||
type SerializedCodeNode = Spread<
|
||||
{
|
||||
language: string | null | undefined;
|
||||
type: 'code';
|
||||
version: 1;
|
||||
},
|
||||
SerializedElementNode
|
||||
>;
|
||||
|
||||
type SerializedCodeHighlightNode = Spread<
|
||||
{
|
||||
highlightType: string | null | undefined;
|
||||
type: 'code-highlight';
|
||||
version: 1;
|
||||
},
|
||||
SerializedTextNode
|
||||
>;
|
||||
|
@ -18,6 +18,8 @@ import type {
|
||||
NodeKey,
|
||||
ParagraphNode,
|
||||
RangeSelection,
|
||||
SerializedElementNode,
|
||||
SerializedTextNode,
|
||||
} from 'lexical';
|
||||
|
||||
import * as Prism from 'prismjs';
|
||||
@ -39,6 +41,7 @@ import {
|
||||
mergeRegister,
|
||||
removeClassNamesFromElement,
|
||||
} from '@lexical/utils';
|
||||
import {Spread} from 'globals';
|
||||
import {
|
||||
$createLineBreakNode,
|
||||
$createParagraphNode,
|
||||
@ -59,6 +62,24 @@ import {
|
||||
|
||||
const DEFAULT_CODE_LANGUAGE = 'javascript';
|
||||
|
||||
type SerializedCodeNode = Spread<
|
||||
{
|
||||
language: string | null | undefined;
|
||||
type: 'code';
|
||||
version: 1;
|
||||
},
|
||||
SerializedElementNode
|
||||
>;
|
||||
|
||||
type SerializedCodeHighlightNode = Spread<
|
||||
{
|
||||
highlightType: string | null | undefined;
|
||||
type: 'code-highlight';
|
||||
version: 1;
|
||||
},
|
||||
SerializedTextNode
|
||||
>;
|
||||
|
||||
const mapToPrismLanguage = (
|
||||
language: string | null | undefined,
|
||||
): string | null | undefined => {
|
||||
@ -99,6 +120,11 @@ export class CodeHighlightNode extends TextNode {
|
||||
);
|
||||
}
|
||||
|
||||
getHighlightType(): string | null | undefined {
|
||||
const self = this.getLatest<CodeHighlightNode>();
|
||||
return self.__highlightType;
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
const element = super.createDOM(config);
|
||||
const className = getHighlightThemeClass(
|
||||
@ -134,6 +160,25 @@ export class CodeHighlightNode extends TextNode {
|
||||
return update;
|
||||
}
|
||||
|
||||
static importJSON(
|
||||
serializedNode: SerializedCodeHighlightNode,
|
||||
): CodeHighlightNode {
|
||||
const node = $createCodeHighlightNode(serializedNode.highlightType);
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setDetail(serializedNode.detail);
|
||||
node.setMode(serializedNode.mode);
|
||||
node.setStyle(serializedNode.style);
|
||||
return node;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedCodeHighlightNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
highlightType: this.getHighlightType(),
|
||||
type: 'code-highlight',
|
||||
};
|
||||
}
|
||||
|
||||
// Prevent formatting (bold, underline, etc)
|
||||
setFormat(format: number): this {
|
||||
return this;
|
||||
@ -266,6 +311,22 @@ export class CodeNode extends ElementNode {
|
||||
};
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedCodeNode): CodeNode {
|
||||
const node = $createCodeNode(serializedNode.language);
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setIndent(serializedNode.indent);
|
||||
node.setDirection(serializedNode.direction);
|
||||
return node;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedCodeNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
language: this.getLanguage(),
|
||||
type: 'code',
|
||||
};
|
||||
}
|
||||
|
||||
// Mutation
|
||||
insertNewAfter(
|
||||
selection: RangeSelection,
|
||||
|
8
packages/lexical-hashtag/LexicalHashtag.d.ts
vendored
8
packages/lexical-hashtag/LexicalHashtag.d.ts
vendored
@ -6,7 +6,12 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import type {EditorConfig, LexicalNode, NodeKey} from 'lexical';
|
||||
import type {
|
||||
EditorConfig,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedTextNode,
|
||||
} from 'lexical';
|
||||
import {TextNode} from 'lexical';
|
||||
|
||||
export declare class HashtagNode extends TextNode {
|
||||
@ -16,6 +21,7 @@ export declare class HashtagNode extends TextNode {
|
||||
createDOM(config: EditorConfig): HTMLElement;
|
||||
canInsertTextBefore(): boolean;
|
||||
isTextEntity(): true;
|
||||
static importJSON(serializedNode: SerializedTextNode): HashtagNode;
|
||||
}
|
||||
export function $createHashtagNode(text?: string): TextNode;
|
||||
export function $isHashtagNode(
|
||||
|
@ -7,17 +7,24 @@
|
||||
* @flow strict
|
||||
*/
|
||||
|
||||
import type {EditorConfig, LexicalNode, NodeKey} from 'lexical';
|
||||
import type {
|
||||
EditorConfig,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedTextNode,
|
||||
} from 'lexical';
|
||||
|
||||
import {TextNode} from 'lexical';
|
||||
|
||||
declare export class HashtagNode extends TextNode {
|
||||
static getType(): string;
|
||||
static clone(node: HashtagNode): HashtagNode;
|
||||
static importJSON(serializedNode: SerializedTextNode): HashtagNode;
|
||||
constructor(text: string, key?: NodeKey): void;
|
||||
createDOM(config: EditorConfig): HTMLElement;
|
||||
canInsertTextBefore(): boolean;
|
||||
isTextEntity(): true;
|
||||
exportJSON(): SerializedTextNode;
|
||||
}
|
||||
declare export function $createHashtagNode(text?: string): HashtagNode;
|
||||
declare export function $isHashtagNode(
|
||||
|
@ -7,7 +7,12 @@
|
||||
* @flow strict
|
||||
*/
|
||||
|
||||
import type {EditorConfig, LexicalNode, NodeKey} from 'lexical';
|
||||
import type {
|
||||
EditorConfig,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedTextNode,
|
||||
} from 'lexical';
|
||||
|
||||
import {addClassNamesToElement} from '@lexical/utils';
|
||||
import {TextNode} from 'lexical';
|
||||
@ -31,6 +36,22 @@ export class HashtagNode extends TextNode {
|
||||
return element;
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedTextNode): HashtagNode {
|
||||
const node = $createHashtagNode(serializedNode.text);
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setDetail(serializedNode.detail);
|
||||
node.setMode(serializedNode.mode);
|
||||
node.setStyle(serializedNode.style);
|
||||
return node;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedTextNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'hashtag',
|
||||
};
|
||||
}
|
||||
|
||||
canInsertTextBefore(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
@ -15,10 +15,20 @@ import type {
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
RangeSelection,
|
||||
SerializedElementNode,
|
||||
} from 'lexical';
|
||||
|
||||
import {addClassNamesToElement} from '@lexical/utils';
|
||||
import {$isElementNode, createCommand, ElementNode} from 'lexical';
|
||||
import invariant from 'shared/invariant';
|
||||
|
||||
export type SerializedLinkNode = {
|
||||
...SerializedElementNode,
|
||||
type: 'link',
|
||||
url: string,
|
||||
version: 1,
|
||||
...
|
||||
};
|
||||
|
||||
export class LinkNode extends ElementNode {
|
||||
__url: string;
|
||||
@ -67,6 +77,22 @@ export class LinkNode extends ElementNode {
|
||||
};
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedLinkNode): LinkNode {
|
||||
const node = $createLinkNode(serializedNode.url);
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setIndent(serializedNode.indent);
|
||||
node.setDirection(serializedNode.direction);
|
||||
return node;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedElementNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'link',
|
||||
url: this.getURL(),
|
||||
};
|
||||
}
|
||||
|
||||
getURL(): string {
|
||||
return this.getLatest().__url;
|
||||
}
|
||||
@ -119,6 +145,13 @@ export function $isLinkNode(node: ?LexicalNode): boolean %checks {
|
||||
return node instanceof LinkNode;
|
||||
}
|
||||
|
||||
export type SerializedAutoLinkNode = {
|
||||
...SerializedLinkNode,
|
||||
type: 'autolink',
|
||||
version: 1,
|
||||
...
|
||||
};
|
||||
|
||||
// Custom node type to override `canInsertTextAfter` that will
|
||||
// allow typing within the link
|
||||
export class AutoLinkNode extends LinkNode {
|
||||
@ -131,6 +164,28 @@ export class AutoLinkNode extends LinkNode {
|
||||
return new AutoLinkNode(node.__url, node.__key);
|
||||
}
|
||||
|
||||
static importJSON(
|
||||
serializedNode: SerializedLinkNode | SerializedAutoLinkNode,
|
||||
): AutoLinkNode {
|
||||
invariant(
|
||||
serializedNode.type !== 'autolink',
|
||||
'Incorrect node type received in importJSON for %s',
|
||||
this.getType(),
|
||||
);
|
||||
const node = $createAutoLinkNode(serializedNode.url);
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setIndent(serializedNode.indent);
|
||||
node.setDirection(serializedNode.direction);
|
||||
return node;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedElementNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'autolink',
|
||||
};
|
||||
}
|
||||
|
||||
insertNewAfter(selection: RangeSelection): null | ElementNode {
|
||||
const element = this.getParentOrThrow().insertNewAfter(selection);
|
||||
if ($isElementNode(element)) {
|
||||
|
26
packages/lexical-list/LexicalList.d.ts
vendored
26
packages/lexical-list/LexicalList.d.ts
vendored
@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import {ListNodeTagType} from './src/LexicalListNode';
|
||||
import {Spread} from 'globals';
|
||||
import {
|
||||
ElementNode,
|
||||
LexicalNode,
|
||||
@ -14,6 +15,7 @@ import {
|
||||
ParagraphNode,
|
||||
RangeSelection,
|
||||
LexicalCommand,
|
||||
SerializedElementNode,
|
||||
} from 'lexical';
|
||||
|
||||
export type ListType = 'number' | 'bullet' | 'check';
|
||||
@ -40,13 +42,18 @@ export declare class ListItemNode extends ElementNode {
|
||||
getChecked(): boolean | void;
|
||||
setChecked(boolean): this;
|
||||
toggleChecked(): void;
|
||||
static importJSON(serializedNode: SerializedListItemNode): ListItemNode;
|
||||
exportJSON(): SerializedListItemNode;
|
||||
}
|
||||
export declare class ListNode extends ElementNode {
|
||||
canBeEmpty(): false;
|
||||
append(...nodesToAppend: LexicalNode[]): ListNode;
|
||||
getTag(): ListNodeTagType;
|
||||
getListType(): ListType;
|
||||
static importJSON(serializedNode: SerializedListNode): ListNode;
|
||||
exportJSON(): SerializedListNode;
|
||||
}
|
||||
|
||||
export function outdentList(): void;
|
||||
export function removeList(editor: LexicalEditor): boolean;
|
||||
|
||||
@ -54,3 +61,22 @@ export var INSERT_UNORDERED_LIST_COMMAND: LexicalCommand<void>;
|
||||
export var INSERT_ORDERED_LIST_COMMAND: LexicalCommand<void>;
|
||||
export var INSERT_CHECK_LIST_COMMAND: LexicalCommand<void>;
|
||||
export var REMOVE_LIST_COMMAND: LexicalCommand<void>;
|
||||
|
||||
export type SerializedListItemNode = Spread<
|
||||
{
|
||||
checked: boolean | void;
|
||||
value: number;
|
||||
type: 'listitem';
|
||||
},
|
||||
SerializedElementNode
|
||||
>;
|
||||
|
||||
export type SerializedListNode = Spread<
|
||||
{
|
||||
listType: ListType;
|
||||
start: number;
|
||||
tag: ListNodeTagType;
|
||||
type: 'list';
|
||||
},
|
||||
SerializedElementNode
|
||||
>;
|
||||
|
@ -13,6 +13,7 @@ import type {
|
||||
ParagraphNode,
|
||||
RangeSelection,
|
||||
LexicalCommand,
|
||||
SerializedElementNode,
|
||||
} from 'lexical';
|
||||
import {ElementNode} from 'lexical';
|
||||
|
||||
@ -53,6 +54,7 @@ declare export class ListItemNode extends ElementNode {
|
||||
getChecked(): boolean | void;
|
||||
setChecked(boolean): this;
|
||||
toggleChecked(): void;
|
||||
static importJSON(serializedNode: SerializedListItemNode): ListItemNode;
|
||||
}
|
||||
declare export class ListNode extends ElementNode {
|
||||
__tag: ListNodeTagType;
|
||||
@ -62,6 +64,7 @@ declare export class ListNode extends ElementNode {
|
||||
getTag(): ListNodeTagType;
|
||||
getStart(): number;
|
||||
getListType(): ListType;
|
||||
static importJSON(serializedNode: SerializedListNode): ListNode;
|
||||
}
|
||||
declare export function outdentList(): void;
|
||||
declare export function removeList(editor: LexicalEditor): boolean;
|
||||
@ -70,3 +73,22 @@ declare export var INSERT_UNORDERED_LIST_COMMAND: LexicalCommand<void>;
|
||||
declare export var INSERT_ORDERED_LIST_COMMAND: LexicalCommand<void>;
|
||||
declare export var INSERT_CHECK_LIST_COMMAND: LexicalCommand<void>;
|
||||
declare export var REMOVE_LIST_COMMAND: LexicalCommand<void>;
|
||||
|
||||
export type SerializedListItemNode = {
|
||||
...SerializedElementNode,
|
||||
checked: boolean | void,
|
||||
value: number,
|
||||
type: 'listitem',
|
||||
version: 1,
|
||||
...
|
||||
};
|
||||
|
||||
export type SerializedListNode = {
|
||||
...SerializedElementNode,
|
||||
listType: ListType,
|
||||
start: number,
|
||||
tag: ListNodeTagType,
|
||||
type: 'list',
|
||||
version: 1,
|
||||
...
|
||||
};
|
||||
|
@ -19,6 +19,7 @@ import type {
|
||||
NodeSelection,
|
||||
ParagraphNode,
|
||||
RangeSelection,
|
||||
SerializedElementNode,
|
||||
} from 'lexical';
|
||||
|
||||
import {
|
||||
@ -41,6 +42,15 @@ import {
|
||||
updateChildrenListItemValue,
|
||||
} from './formatList';
|
||||
|
||||
export type SerializedListItemNode = {
|
||||
...SerializedElementNode,
|
||||
checked: boolean | void,
|
||||
type: 'listitem',
|
||||
value: number,
|
||||
version: 1,
|
||||
...
|
||||
};
|
||||
|
||||
export class ListItemNode extends ElementNode {
|
||||
__value: number;
|
||||
__checked: boolean | void;
|
||||
@ -96,6 +106,23 @@ export class ListItemNode extends ElementNode {
|
||||
};
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedListItemNode): ListItemNode {
|
||||
const node = new ListItemNode(serializedNode.value, serializedNode.checked);
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setIndent(serializedNode.indent);
|
||||
node.setDirection(serializedNode.direction);
|
||||
return node;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedElementNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
checked: this.getChecked(),
|
||||
type: 'listitem',
|
||||
value: this.getValue(),
|
||||
};
|
||||
}
|
||||
|
||||
append(...nodes: LexicalNode[]): ListItemNode {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
@ -262,8 +289,13 @@ export class ListItemNode extends ElementNode {
|
||||
}
|
||||
|
||||
getIndent(): number {
|
||||
// If we don't have a parent, we are likely serializing
|
||||
const parent = this.getParent();
|
||||
if (parent === null) {
|
||||
return this.getLatest().__indent;
|
||||
}
|
||||
// ListItemNode should always have a ListNode for a parent.
|
||||
let listNodeParent = this.getParentOrThrow().getParentOrThrow();
|
||||
let listNodeParent = parent.getParentOrThrow();
|
||||
let indentLevel = 0;
|
||||
while ($isListItemNode(listNodeParent)) {
|
||||
listNodeParent = listNodeParent.getParentOrThrow().getParentOrThrow();
|
||||
|
@ -14,6 +14,7 @@ import type {
|
||||
EditorThemeClasses,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedElementNode,
|
||||
} from 'lexical';
|
||||
|
||||
import {
|
||||
@ -25,6 +26,16 @@ import {$createTextNode, ElementNode} from 'lexical';
|
||||
import {$createListItemNode, $isListItemNode} from '.';
|
||||
import {$getListDepth} from './utils';
|
||||
|
||||
export type SerializedListNode = {
|
||||
...SerializedElementNode,
|
||||
listType: ListType,
|
||||
start: number,
|
||||
tag: ListNodeTagType,
|
||||
type: 'list',
|
||||
version: 1,
|
||||
...
|
||||
};
|
||||
|
||||
export type ListType = 'number' | 'bullet' | 'check';
|
||||
|
||||
export type ListNodeTagType = 'ul' | 'ol';
|
||||
@ -103,6 +114,24 @@ export class ListNode extends ElementNode {
|
||||
};
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedListNode): ListNode {
|
||||
const node = $createListNode(serializedNode.listType, serializedNode.start);
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setIndent(serializedNode.indent);
|
||||
node.setDirection(serializedNode.direction);
|
||||
return node;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedElementNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
listType: this.getListType(),
|
||||
start: this.getStart(),
|
||||
tag: this.getTag(),
|
||||
type: 'list',
|
||||
};
|
||||
}
|
||||
|
||||
canBeEmpty(): false {
|
||||
return false;
|
||||
}
|
||||
|
19
packages/lexical-overflow/LexicalOverflow.d.ts
vendored
19
packages/lexical-overflow/LexicalOverflow.d.ts
vendored
@ -6,7 +6,14 @@
|
||||
*
|
||||
* @flow strict
|
||||
*/
|
||||
import type {EditorConfig, LexicalNode, NodeKey, RangeSelection} from 'lexical';
|
||||
import type {
|
||||
EditorConfig,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
RangeSelection,
|
||||
SerializedElementNode,
|
||||
} from 'lexical';
|
||||
import {Spread} from 'globals';
|
||||
import {ElementNode} from 'lexical';
|
||||
export declare class OverflowNode extends ElementNode {
|
||||
static getType(): string;
|
||||
@ -16,6 +23,16 @@ export declare class OverflowNode extends ElementNode {
|
||||
updateDOM(prevNode: OverflowNode, dom: HTMLElement): boolean;
|
||||
insertNewAfter(selection: RangeSelection): null | LexicalNode;
|
||||
excludeFromCopy(): boolean;
|
||||
static importJSON(serializedNode: SerializedOverflowNode): OverflowNode;
|
||||
exportJSON(): SerializedElementNode;
|
||||
}
|
||||
|
||||
export function $createOverflowNode(): OverflowNode;
|
||||
export function $isOverflowNode(node: LexicalNode | null): node is OverflowNode;
|
||||
|
||||
export type SerializedOverflowNode = Spread<
|
||||
{
|
||||
type: 'overflow';
|
||||
},
|
||||
SerializedElementNode
|
||||
>;
|
||||
|
@ -6,7 +6,13 @@
|
||||
*
|
||||
* @flow strict
|
||||
*/
|
||||
import type {EditorConfig, LexicalNode, NodeKey, RangeSelection} from 'lexical';
|
||||
import type {
|
||||
EditorConfig,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
RangeSelection,
|
||||
SerializedElementNode,
|
||||
} from 'lexical';
|
||||
import {ElementNode} from 'lexical';
|
||||
declare export class OverflowNode extends ElementNode {
|
||||
static getType(): string;
|
||||
@ -16,8 +22,16 @@ declare export class OverflowNode extends ElementNode {
|
||||
updateDOM(prevNode: OverflowNode, dom: HTMLElement): boolean;
|
||||
insertNewAfter(selection: RangeSelection): null | LexicalNode;
|
||||
excludeFromCopy(): boolean;
|
||||
static importJSON(serializedNode: SerializedOverflowNode): OverflowNode;
|
||||
}
|
||||
declare export function $createOverflowNode(): OverflowNode;
|
||||
declare export function $isOverflowNode(
|
||||
node: ?LexicalNode,
|
||||
): boolean %checks(node instanceof OverflowNode);
|
||||
|
||||
export type SerializedOverflowNode = {
|
||||
...SerializedElementNode,
|
||||
type: 'overflow',
|
||||
version: 1,
|
||||
...
|
||||
};
|
||||
|
@ -7,10 +7,23 @@
|
||||
* @flow strict
|
||||
*/
|
||||
|
||||
import type {EditorConfig, LexicalNode, NodeKey, RangeSelection} from 'lexical';
|
||||
import type {
|
||||
EditorConfig,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
RangeSelection,
|
||||
SerializedElementNode,
|
||||
} from 'lexical';
|
||||
|
||||
import {ElementNode} from 'lexical';
|
||||
|
||||
export type SerializedOverflowNode = {
|
||||
...SerializedElementNode,
|
||||
type: 'overflow',
|
||||
version: 1,
|
||||
...
|
||||
};
|
||||
|
||||
export class OverflowNode extends ElementNode {
|
||||
static getType(): string {
|
||||
return 'overflow';
|
||||
@ -20,11 +33,22 @@ export class OverflowNode extends ElementNode {
|
||||
return new OverflowNode(node.__key);
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedOverflowNode): OverflowNode {
|
||||
return $createOverflowNode();
|
||||
}
|
||||
|
||||
constructor(key?: NodeKey): void {
|
||||
super(key);
|
||||
this.__type = 'overflow';
|
||||
}
|
||||
|
||||
exportJSON(): SerializedElementNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'overflow',
|
||||
};
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
const div = document.createElement('span');
|
||||
const className = config.theme.characterLimit;
|
||||
|
@ -6,12 +6,26 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import type {EditorConfig, LexicalNode, NodeKey} from 'lexical';
|
||||
import type {
|
||||
EditorConfig,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedTextNode,
|
||||
} from 'lexical';
|
||||
|
||||
import {Spread} from 'globals';
|
||||
import {TextNode} from 'lexical';
|
||||
|
||||
export type SerializedEmojiNode = Spread<
|
||||
{
|
||||
className: string;
|
||||
type: 'emoji';
|
||||
},
|
||||
SerializedTextNode
|
||||
>;
|
||||
|
||||
export class EmojiNode extends TextNode {
|
||||
__className?: string;
|
||||
__className: string;
|
||||
|
||||
static getType(): string {
|
||||
return 'emoji';
|
||||
@ -47,6 +61,31 @@ export class EmojiNode extends TextNode {
|
||||
super.updateDOM(prevNode, inner as HTMLElement, config);
|
||||
return false;
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedEmojiNode): EmojiNode {
|
||||
const node = $createEmojiNode(
|
||||
serializedNode.className,
|
||||
serializedNode.text,
|
||||
);
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setDetail(serializedNode.detail);
|
||||
node.setMode(serializedNode.mode);
|
||||
node.setStyle(serializedNode.style);
|
||||
return node;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedEmojiNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
className: this.getClassName(),
|
||||
type: 'emoji',
|
||||
};
|
||||
}
|
||||
|
||||
getClassName(): string {
|
||||
const self = this.getLatest<EmojiNode>();
|
||||
return self.__className;
|
||||
}
|
||||
}
|
||||
|
||||
export function $isEmojiNode(
|
||||
|
@ -11,10 +11,12 @@ import type {
|
||||
EditorConfig,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedLexicalNode,
|
||||
} from 'lexical';
|
||||
|
||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||
import {mergeRegister} from '@lexical/utils';
|
||||
import {Spread} from 'globals';
|
||||
import {
|
||||
$getNodeByKey,
|
||||
COMMAND_PRIORITY_HIGH,
|
||||
@ -114,6 +116,15 @@ function EquationComponent({
|
||||
);
|
||||
}
|
||||
|
||||
export type SerializedEquationNode = Spread<
|
||||
{
|
||||
type: 'equation';
|
||||
equation: string;
|
||||
inline: boolean;
|
||||
},
|
||||
SerializedLexicalNode
|
||||
>;
|
||||
|
||||
export class EquationNode extends DecoratorNode<JSX.Element> {
|
||||
__equation: string;
|
||||
__inline: boolean;
|
||||
@ -132,6 +143,23 @@ export class EquationNode extends DecoratorNode<JSX.Element> {
|
||||
this.__inline = inline ?? false;
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedEquationNode): EquationNode {
|
||||
const node = $createEquationNode(
|
||||
serializedNode.equation,
|
||||
serializedNode.inline,
|
||||
);
|
||||
return node;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedEquationNode {
|
||||
return {
|
||||
equation: this.getEquation(),
|
||||
inline: this.__inline,
|
||||
type: 'emoji',
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
exportDOM(): DOMExportOutput {
|
||||
const element = document.createElement(this.__inline ? 'span' : 'div');
|
||||
element.innerText = this.__equation;
|
||||
|
@ -7,11 +7,18 @@
|
||||
*/
|
||||
|
||||
import type {ExcalidrawElementFragment} from './ExcalidrawModal';
|
||||
import type {EditorConfig, LexicalEditor, LexicalNode, NodeKey} from 'lexical';
|
||||
import type {
|
||||
EditorConfig,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedLexicalNode,
|
||||
} from 'lexical';
|
||||
|
||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||
import {useLexicalNodeSelection} from '@lexical/react/useLexicalNodeSelection';
|
||||
import {mergeRegister} from '@lexical/utils';
|
||||
import {Spread} from 'globals';
|
||||
import {
|
||||
$getNodeByKey,
|
||||
$getSelection,
|
||||
@ -193,6 +200,15 @@ function ExcalidrawComponent({
|
||||
);
|
||||
}
|
||||
|
||||
export type SerializedExcalidrawNode = Spread<
|
||||
{
|
||||
data: string;
|
||||
type: 'excalidraw';
|
||||
version: 1;
|
||||
},
|
||||
SerializedLexicalNode
|
||||
>;
|
||||
|
||||
export class ExcalidrawNode extends DecoratorNode<JSX.Element> {
|
||||
__data: string;
|
||||
|
||||
@ -204,6 +220,18 @@ export class ExcalidrawNode extends DecoratorNode<JSX.Element> {
|
||||
return new ExcalidrawNode(node.__data, node.__key);
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedExcalidrawNode): ExcalidrawNode {
|
||||
return new ExcalidrawNode(serializedNode.data);
|
||||
}
|
||||
|
||||
exportJSON(): SerializedExcalidrawNode {
|
||||
return {
|
||||
data: this.__data,
|
||||
type: 'excalidraw',
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(data = '[]', key?: NodeKey) {
|
||||
super(key);
|
||||
this.__data = data;
|
||||
|
@ -12,6 +12,7 @@ import type {
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedLexicalNode,
|
||||
} from 'lexical';
|
||||
|
||||
import './ImageNode.css';
|
||||
@ -29,6 +30,7 @@ import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
|
||||
import {TablePlugin} from '@lexical/react/LexicalTablePlugin';
|
||||
import {useLexicalNodeSelection} from '@lexical/react/useLexicalNodeSelection';
|
||||
import {mergeRegister} from '@lexical/utils';
|
||||
import {Spread} from 'globals';
|
||||
import {
|
||||
$getNodeByKey,
|
||||
$getSelection,
|
||||
@ -310,6 +312,21 @@ function ImageComponent({
|
||||
);
|
||||
}
|
||||
|
||||
export type SerializedImageNode = Spread<
|
||||
{
|
||||
altText: string;
|
||||
caption: LexicalEditor;
|
||||
height?: number;
|
||||
maxWidth: number;
|
||||
showCaption: boolean;
|
||||
src: string;
|
||||
width?: number;
|
||||
type: 'image';
|
||||
version: 1;
|
||||
},
|
||||
SerializedLexicalNode
|
||||
>;
|
||||
|
||||
export class ImageNode extends DecoratorNode<JSX.Element> {
|
||||
__src: string;
|
||||
__altText: string;
|
||||
@ -336,6 +353,20 @@ export class ImageNode extends DecoratorNode<JSX.Element> {
|
||||
);
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedImageNode): ImageNode {
|
||||
const {altText, height, width, maxWidth, caption, src, showCaption} =
|
||||
serializedNode;
|
||||
return $createImageNode({
|
||||
altText,
|
||||
caption,
|
||||
height,
|
||||
maxWidth,
|
||||
showCaption,
|
||||
src,
|
||||
width,
|
||||
});
|
||||
}
|
||||
|
||||
constructor(
|
||||
src: string,
|
||||
altText: string,
|
||||
@ -363,6 +394,20 @@ export class ImageNode extends DecoratorNode<JSX.Element> {
|
||||
return {element};
|
||||
}
|
||||
|
||||
exportJSON(): SerializedImageNode {
|
||||
return {
|
||||
altText: this.getAltText(),
|
||||
caption: this.__caption,
|
||||
height: this.__height === 'inherit' ? 0 : this.__height,
|
||||
maxWidth: this.__maxWidth,
|
||||
showCaption: this.__showCaption,
|
||||
src: this.getSrc(),
|
||||
type: 'image',
|
||||
version: 1,
|
||||
width: this.__width === 'inherit' ? 0 : this.__width,
|
||||
};
|
||||
}
|
||||
|
||||
setWidthAndHeight(
|
||||
width: 'inherit' | number,
|
||||
height: 'inherit' | number,
|
||||
|
@ -6,10 +6,19 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import type {EditorConfig, LexicalNode} from 'lexical';
|
||||
import type {EditorConfig, LexicalNode, SerializedTextNode} from 'lexical';
|
||||
|
||||
import {Spread} from 'globals';
|
||||
import {TextNode} from 'lexical';
|
||||
|
||||
export type SerializedKeywordNode = Spread<
|
||||
{
|
||||
type: 'keyword';
|
||||
version: 1;
|
||||
},
|
||||
SerializedTextNode
|
||||
>;
|
||||
|
||||
export class KeywordNode extends TextNode {
|
||||
static getType(): string {
|
||||
return 'keyword';
|
||||
@ -19,6 +28,23 @@ export class KeywordNode extends TextNode {
|
||||
return new KeywordNode(node.__text, node.__key);
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedKeywordNode): KeywordNode {
|
||||
const node = $createKeywordNode(serializedNode.text);
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setDetail(serializedNode.detail);
|
||||
node.setMode(serializedNode.mode);
|
||||
node.setStyle(serializedNode.style);
|
||||
return node;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedKeywordNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'keyword',
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
const dom = super.createDOM(config);
|
||||
dom.style.cursor = 'default';
|
||||
|
@ -6,10 +6,25 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import type {EditorConfig, LexicalNode, NodeKey} from 'lexical';
|
||||
import type {
|
||||
EditorConfig,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedTextNode,
|
||||
} from 'lexical';
|
||||
|
||||
import {Spread} from 'globals';
|
||||
import {TextNode} from 'lexical';
|
||||
|
||||
export type SerializedMentionNode = Spread<
|
||||
{
|
||||
mentionName: string;
|
||||
type: 'mention';
|
||||
version: 1;
|
||||
},
|
||||
SerializedTextNode
|
||||
>;
|
||||
|
||||
const mentionStyle = 'background-color: rgba(24, 119, 232, 0.2)';
|
||||
export class MentionNode extends TextNode {
|
||||
__mention: string;
|
||||
@ -21,12 +36,30 @@ export class MentionNode extends TextNode {
|
||||
static clone(node: MentionNode): MentionNode {
|
||||
return new MentionNode(node.__mention, node.__text, node.__key);
|
||||
}
|
||||
static importJSON(serializedNode: SerializedMentionNode): MentionNode {
|
||||
const node = $createMentionNode(serializedNode.mentionName);
|
||||
node.setTextContent(serializedNode.text);
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setDetail(serializedNode.detail);
|
||||
node.setMode(serializedNode.mode);
|
||||
node.setStyle(serializedNode.style);
|
||||
return node;
|
||||
}
|
||||
|
||||
constructor(mentionName: string, text?: string, key?: NodeKey) {
|
||||
super(text ?? mentionName, key);
|
||||
this.__mention = mentionName;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedMentionNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
mentionName: this.__mention,
|
||||
type: 'mention',
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
const dom = super.createDOM(config);
|
||||
dom.style.cssText = mentionStyle;
|
||||
|
@ -6,12 +6,13 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import type {LexicalNode, NodeKey} from 'lexical';
|
||||
import type {LexicalNode, NodeKey, SerializedLexicalNode} from 'lexical';
|
||||
|
||||
import './PollNode.css';
|
||||
|
||||
import {useCollaborationContext} from '@lexical/react/LexicalCollaborationPlugin';
|
||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||
import {Spread} from 'globals';
|
||||
import {$getNodeByKey, DecoratorNode} from 'lexical';
|
||||
import * as React from 'react';
|
||||
import {useMemo, useRef} from 'react';
|
||||
@ -189,6 +190,16 @@ function PollComponent({
|
||||
);
|
||||
}
|
||||
|
||||
export type SerializedPollNode = Spread<
|
||||
{
|
||||
question: string;
|
||||
options: Options;
|
||||
type: 'poll';
|
||||
version: 1;
|
||||
},
|
||||
SerializedLexicalNode
|
||||
>;
|
||||
|
||||
export class PollNode extends DecoratorNode<JSX.Element> {
|
||||
__question: string;
|
||||
__options: Options;
|
||||
@ -201,12 +212,27 @@ export class PollNode extends DecoratorNode<JSX.Element> {
|
||||
return new PollNode(node.__question, node.__options, node.__key);
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedPollNode): PollNode {
|
||||
const node = $createPollNode(serializedNode.question);
|
||||
serializedNode.options.forEach(node.addOption);
|
||||
return node;
|
||||
}
|
||||
|
||||
constructor(question: string, options?: Options, key?: NodeKey) {
|
||||
super(key);
|
||||
this.__question = question;
|
||||
this.__options = options || [createPollOption(), createPollOption()];
|
||||
}
|
||||
|
||||
exportJSON(): SerializedPollNode {
|
||||
return {
|
||||
options: this.__options,
|
||||
question: this.__question,
|
||||
type: 'poll',
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
addOption(option: Option): void {
|
||||
const self = this.getWritable<PollNode>();
|
||||
const options = Array.from(self.__options);
|
||||
|
@ -6,7 +6,13 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import type {EditorConfig, LexicalEditor, LexicalNode, NodeKey} from 'lexical';
|
||||
import type {
|
||||
EditorConfig,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedLexicalNode,
|
||||
} from 'lexical';
|
||||
|
||||
import './StickyNode.css';
|
||||
|
||||
@ -18,6 +24,7 @@ import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
|
||||
import {LexicalNestedComposer} from '@lexical/react/LexicalNestedComposer';
|
||||
import {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin';
|
||||
import {Spread} from 'globals';
|
||||
import {
|
||||
$getNodeByKey,
|
||||
$setSelection,
|
||||
@ -266,10 +273,24 @@ function StickyComponent({
|
||||
);
|
||||
}
|
||||
|
||||
type StickyNoteColor = 'pink' | 'yellow';
|
||||
|
||||
export type SerializedStickyNode = Spread<
|
||||
{
|
||||
xOffset: number;
|
||||
yOffset: number;
|
||||
color: StickyNoteColor;
|
||||
caption: LexicalEditor;
|
||||
type: 'sticky';
|
||||
version: 1;
|
||||
},
|
||||
SerializedLexicalNode
|
||||
>;
|
||||
|
||||
export class StickyNode extends DecoratorNode<JSX.Element> {
|
||||
__x: number;
|
||||
__y: number;
|
||||
__color: 'pink' | 'yellow';
|
||||
__color: StickyNoteColor;
|
||||
__caption: LexicalEditor;
|
||||
|
||||
static getType(): string {
|
||||
@ -285,6 +306,14 @@ export class StickyNode extends DecoratorNode<JSX.Element> {
|
||||
node.__key,
|
||||
);
|
||||
}
|
||||
static importJSON(serializedNode: SerializedStickyNode): StickyNode {
|
||||
return new StickyNode(
|
||||
serializedNode.xOffset,
|
||||
serializedNode.yOffset,
|
||||
serializedNode.color,
|
||||
serializedNode.caption,
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
x: number,
|
||||
@ -300,6 +329,17 @@ export class StickyNode extends DecoratorNode<JSX.Element> {
|
||||
this.__color = color;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedStickyNode {
|
||||
return {
|
||||
caption: this.__caption,
|
||||
color: this.__color,
|
||||
type: 'sticky',
|
||||
version: 1,
|
||||
xOffset: this.__x,
|
||||
yOffset: this.__y,
|
||||
};
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
const div = document.createElement('div');
|
||||
div.style.display = 'contents';
|
||||
|
@ -9,7 +9,11 @@
|
||||
import type {ElementFormatType, LexicalNode, NodeKey} from 'lexical';
|
||||
|
||||
import {BlockWithAlignableContents} from '@lexical/react/LexicalBlockWithAlignableContents';
|
||||
import {DecoratorBlockNode} from '@lexical/react/LexicalDecoratorBlockNode';
|
||||
import {
|
||||
DecoratorBlockNode,
|
||||
SerializedDecoratorBlockNode,
|
||||
} from '@lexical/react/LexicalDecoratorBlockNode';
|
||||
import {Spread} from 'libdefs/globals';
|
||||
import * as React from 'react';
|
||||
import {useCallback, useEffect, useRef, useState} from 'react';
|
||||
|
||||
@ -42,6 +46,7 @@ function TweetComponent({
|
||||
|
||||
const createTweet = useCallback(async () => {
|
||||
try {
|
||||
// @ts-expect-error Twitter is attached to the window.
|
||||
await window.twttr.widgets.createTweet(tweetID, containerRef.current);
|
||||
|
||||
setIsLoading(false);
|
||||
@ -88,6 +93,15 @@ function TweetComponent({
|
||||
);
|
||||
}
|
||||
|
||||
export type SerializedTweetNode = Spread<
|
||||
{
|
||||
id: string;
|
||||
type: 'tweet';
|
||||
version: 1;
|
||||
},
|
||||
SerializedDecoratorBlockNode
|
||||
>;
|
||||
|
||||
export class TweetNode extends DecoratorBlockNode<JSX.Element> {
|
||||
__id: string;
|
||||
|
||||
@ -99,6 +113,21 @@ export class TweetNode extends DecoratorBlockNode<JSX.Element> {
|
||||
return new TweetNode(node.__id, node.__format, node.__key);
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedTweetNode): TweetNode {
|
||||
const node = $createTweetNode(serializedNode.id);
|
||||
node.setFormat(serializedNode.format);
|
||||
return node;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedTweetNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
id: this.getId(),
|
||||
type: 'tweet',
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(id: string, format?: ElementFormatType | null, key?: NodeKey) {
|
||||
super(format, key);
|
||||
this.__id = id;
|
||||
|
@ -6,10 +6,19 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import type {EditorConfig} from 'lexical';
|
||||
import type {EditorConfig, SerializedTextNode} from 'lexical';
|
||||
|
||||
import {Spread} from 'globals';
|
||||
import {TextNode} from 'lexical';
|
||||
|
||||
export type SerializedTypeaheadNode = Spread<
|
||||
{
|
||||
type: 'typeahead';
|
||||
version: 1;
|
||||
},
|
||||
SerializedTextNode
|
||||
>;
|
||||
|
||||
export class TypeaheadNode extends TextNode {
|
||||
static clone(node: TypeaheadNode): TypeaheadNode {
|
||||
return new TypeaheadNode(node.__text, node.__key);
|
||||
@ -19,6 +28,23 @@ export class TypeaheadNode extends TextNode {
|
||||
return 'typeahead';
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedTypeaheadNode): TypeaheadNode {
|
||||
const node = $createTypeaheadNode(serializedNode.text);
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setDetail(serializedNode.detail);
|
||||
node.setMode(serializedNode.mode);
|
||||
node.setStyle(serializedNode.style);
|
||||
return node;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedTypeaheadNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'typeahead',
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
const dom = super.createDOM(config);
|
||||
dom.style.cssText = 'color: #ccc;';
|
||||
|
@ -9,7 +9,11 @@
|
||||
import type {ElementFormatType, LexicalNode, NodeKey} from 'lexical';
|
||||
|
||||
import {BlockWithAlignableContents} from '@lexical/react/LexicalBlockWithAlignableContents';
|
||||
import {DecoratorBlockNode} from '@lexical/react/LexicalDecoratorBlockNode';
|
||||
import {
|
||||
DecoratorBlockNode,
|
||||
SerializedDecoratorBlockNode,
|
||||
} from '@lexical/react/LexicalDecoratorBlockNode';
|
||||
import {Spread} from 'libdefs/globals';
|
||||
import * as React from 'react';
|
||||
|
||||
type YouTubeComponentProps = Readonly<{
|
||||
@ -34,6 +38,15 @@ function YouTubeComponent({format, nodeKey, videoID}: YouTubeComponentProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export type SerializedYouTubeNode = Spread<
|
||||
{
|
||||
videoID: string;
|
||||
type: 'youtube';
|
||||
version: 1;
|
||||
},
|
||||
SerializedDecoratorBlockNode
|
||||
>;
|
||||
|
||||
export class YouTubeNode extends DecoratorBlockNode<JSX.Element> {
|
||||
__id: string;
|
||||
|
||||
@ -45,6 +58,21 @@ export class YouTubeNode extends DecoratorBlockNode<JSX.Element> {
|
||||
return new YouTubeNode(node.__id, node.__format, node.__key);
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedYouTubeNode): YouTubeNode {
|
||||
const node = $createYouTubeNode(serializedNode.videoID);
|
||||
node.setFormat(serializedNode.format);
|
||||
return node;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedYouTubeNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'youtube',
|
||||
version: 1,
|
||||
videoID: this.__id,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(id: string, format?: ElementFormatType | null, key?: NodeKey) {
|
||||
super(format, key);
|
||||
this.__id = id;
|
||||
|
@ -7,15 +7,25 @@
|
||||
* @flow strict
|
||||
*/
|
||||
|
||||
import type {ElementFormatType, LexicalNode, NodeKey} from 'lexical';
|
||||
import type {
|
||||
ElementFormatType,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedLexicalNode,
|
||||
} from 'lexical';
|
||||
|
||||
import {DecoratorNode} from 'lexical';
|
||||
|
||||
export type SerializedDecoratorBlockNode = SerializedLexicalNode & {
|
||||
format: ElementFormatType;
|
||||
};
|
||||
|
||||
declare class DecoratorBlockNode<T> extends DecoratorNode<T> {
|
||||
__format: ElementFormatType;
|
||||
constructor(format?: ElementFormatType | null, key?: NodeKey);
|
||||
createDOM(): HTMLElement;
|
||||
setFormat(format: ElementFormatType): void;
|
||||
exportJSON(): SerializedDecoratorBlockNode;
|
||||
}
|
||||
|
||||
declare function $isDecoratorBlockNode<T>(
|
||||
|
@ -7,10 +7,21 @@
|
||||
* @flow strict
|
||||
*/
|
||||
|
||||
import type {ElementFormatType, LexicalNode, NodeKey} from 'lexical';
|
||||
import type {
|
||||
ElementFormatType,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedLexicalNode,
|
||||
} from 'lexical';
|
||||
|
||||
import {DecoratorNode} from 'lexical';
|
||||
|
||||
export type SerializedDecoratorBlockNode = {
|
||||
...SerializedLexicalNode,
|
||||
format: ElementFormatType,
|
||||
...
|
||||
};
|
||||
|
||||
export class DecoratorBlockNode extends DecoratorNode<React$Node> {
|
||||
__format: ?ElementFormatType;
|
||||
|
||||
@ -19,6 +30,14 @@ export class DecoratorBlockNode extends DecoratorNode<React$Node> {
|
||||
this.__format = format;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedDecoratorBlockNode {
|
||||
return {
|
||||
format: this.__format || '',
|
||||
type: 'decorator-block',
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
createDOM(): HTMLElement {
|
||||
return document.createElement('div');
|
||||
}
|
||||
|
@ -13,11 +13,17 @@ import type {
|
||||
DOMExportOutput,
|
||||
LexicalCommand,
|
||||
LexicalNode,
|
||||
SerializedLexicalNode,
|
||||
} from 'lexical';
|
||||
|
||||
import {createCommand, DecoratorNode} from 'lexical';
|
||||
import * as React from 'react';
|
||||
|
||||
export type SerializedHorizontalRuleNode = SerializedLexicalNode & {
|
||||
type: 'horizontalrule',
|
||||
version: 1,
|
||||
};
|
||||
|
||||
export const INSERT_HORIZONTAL_RULE_COMMAND: LexicalCommand<void> =
|
||||
createCommand();
|
||||
|
||||
@ -47,6 +53,19 @@ export class HorizontalRuleNode extends DecoratorNode<React$Node> {
|
||||
return {element: document.createElement('hr')};
|
||||
}
|
||||
|
||||
static importJSON(
|
||||
serializedNode: SerializedHorizontalRuleNode,
|
||||
): HorizontalRuleNode {
|
||||
return $createHorizontalRuleNode();
|
||||
}
|
||||
|
||||
exportJSON(): SerializedLexicalNode {
|
||||
return {
|
||||
type: 'horizontalrule',
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
createDOM(): HTMLElement {
|
||||
const div = document.createElement('div');
|
||||
div.style.display = 'contents';
|
||||
|
24
packages/lexical-rich-text/LexicalRichText.d.ts
vendored
24
packages/lexical-rich-text/LexicalRichText.d.ts
vendored
@ -5,6 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import type {
|
||||
DOMConversionMap,
|
||||
EditorConfig,
|
||||
@ -13,8 +14,11 @@ import type {
|
||||
NodeKey,
|
||||
ParagraphNode,
|
||||
LexicalEditor,
|
||||
SerializedElementNode,
|
||||
} from 'lexical';
|
||||
import {ElementNode} from 'lexical';
|
||||
import {Spread} from 'libdefs/globals';
|
||||
|
||||
export type InitialEditorStateType = null | string | EditorState | (() => void);
|
||||
|
||||
export declare class QuoteNode extends ElementNode {
|
||||
@ -25,6 +29,7 @@ export declare class QuoteNode extends ElementNode {
|
||||
updateDOM(prevNode: QuoteNode, dom: HTMLElement): boolean;
|
||||
insertNewAfter(): ParagraphNode;
|
||||
collapseAtStart(): true;
|
||||
importJSON(serializedNode: SerializedQuoteNode): QuoteNode;
|
||||
}
|
||||
export function $createQuoteNode(): QuoteNode;
|
||||
export function $isQuoteNode(
|
||||
@ -42,7 +47,9 @@ export declare class HeadingNode extends ElementNode {
|
||||
static importDOM(): DOMConversionMap | null;
|
||||
insertNewAfter(): ParagraphNode;
|
||||
collapseAtStart(): true;
|
||||
importJSON(serializedNode: SerializedHeadingNode): QuoteNode;
|
||||
}
|
||||
|
||||
export function $createHeadingNode(headingTag: HeadingTagType): HeadingNode;
|
||||
export function $isHeadingNode(
|
||||
node: LexicalNode | null | undefined,
|
||||
@ -51,3 +58,20 @@ export function registerRichText(
|
||||
editor: LexicalEditor,
|
||||
initialEditorState?: InitialEditorStateType,
|
||||
): () => void;
|
||||
|
||||
export type SerializedHeadingNode = Spread<
|
||||
{
|
||||
tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5';
|
||||
type: 'heading';
|
||||
version: 1;
|
||||
},
|
||||
SerializedElementNode
|
||||
>;
|
||||
|
||||
export type SerializedQuoteNode = Spread<
|
||||
{
|
||||
type: 'quote';
|
||||
version: 1;
|
||||
},
|
||||
SerializedElementNode
|
||||
>;
|
||||
|
@ -16,6 +16,7 @@ import type {
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
ParagraphNode,
|
||||
SerializedElementNode,
|
||||
TextFormatType,
|
||||
} from 'lexical';
|
||||
|
||||
@ -33,6 +34,7 @@ import {
|
||||
addClassNamesToElement,
|
||||
mergeRegister,
|
||||
} from '@lexical/utils';
|
||||
import {Spread} from 'globals';
|
||||
import {
|
||||
$createParagraphNode,
|
||||
$getRoot,
|
||||
@ -72,6 +74,23 @@ import {CAN_USE_BEFORE_INPUT, IS_IOS, IS_SAFARI} from 'shared-ts/environment';
|
||||
|
||||
export type InitialEditorStateType = null | string | EditorState | (() => void);
|
||||
|
||||
export type SerializedHeadingNode = Spread<
|
||||
{
|
||||
tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5';
|
||||
type: 'heading';
|
||||
version: 1;
|
||||
},
|
||||
SerializedElementNode
|
||||
>;
|
||||
|
||||
export type SerializedQuoteNode = Spread<
|
||||
{
|
||||
type: 'quote';
|
||||
version: 1;
|
||||
},
|
||||
SerializedElementNode
|
||||
>;
|
||||
|
||||
// Convoluted logic to make this work with Flow. Order matters.
|
||||
const options = {tag: 'history-merge'};
|
||||
const setEditorOptions: {
|
||||
@ -107,6 +126,21 @@ export class QuoteNode extends ElementNode {
|
||||
return false;
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedQuoteNode): QuoteNode {
|
||||
const node = $createQuoteNode();
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setIndent(serializedNode.indent);
|
||||
node.setDirection(serializedNode.direction);
|
||||
return node;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedElementNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'quote',
|
||||
};
|
||||
}
|
||||
|
||||
// Mutation
|
||||
|
||||
insertNewAfter(): ParagraphNode {
|
||||
@ -202,6 +236,23 @@ export class HeadingNode extends ElementNode {
|
||||
};
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedHeadingNode): HeadingNode {
|
||||
const node = $createHeadingNode(serializedNode.tag);
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setIndent(serializedNode.indent);
|
||||
node.setDirection(serializedNode.direction);
|
||||
return node;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedHeadingNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
tag: this.__tag,
|
||||
type: 'heading',
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
// Mutation
|
||||
|
||||
insertNewAfter(): ParagraphNode {
|
||||
|
@ -15,6 +15,8 @@ import type {
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedElementNode,
|
||||
SerializedGridCellNode,
|
||||
} from 'lexical';
|
||||
|
||||
import {addClassNamesToElement} from '@lexical/utils';
|
||||
@ -34,6 +36,14 @@ export const TableCellHeaderStates = {
|
||||
|
||||
export type TableCellHeaderState = $Values<typeof TableCellHeaderStates>;
|
||||
|
||||
export type SerializedTableCellNode = {
|
||||
...SerializedGridCellNode,
|
||||
headerState: TableCellHeaderState,
|
||||
type: 'tablecell',
|
||||
width: number,
|
||||
...
|
||||
};
|
||||
|
||||
export class TableCellNode extends GridCellNode {
|
||||
__headerState: TableCellHeaderState;
|
||||
__width: ?number;
|
||||
@ -64,6 +74,14 @@ export class TableCellNode extends GridCellNode {
|
||||
};
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedTableCellNode): TableCellNode {
|
||||
return $createTableCellNode(
|
||||
serializedNode.headerState,
|
||||
serializedNode.colSpan,
|
||||
serializedNode.width,
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
headerState?: TableCellHeaderState = TableCellHeaderStates.NO_STATUS,
|
||||
colSpan?: number = 1,
|
||||
@ -115,6 +133,15 @@ export class TableCellNode extends GridCellNode {
|
||||
};
|
||||
}
|
||||
|
||||
exportJSON(): SerializedElementNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
headerState: this.__headerState,
|
||||
width: this.getWidth(),
|
||||
type: 'tablecell',
|
||||
};
|
||||
}
|
||||
|
||||
getTag(): string {
|
||||
return this.hasHeader() ? 'th' : 'td';
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import type {
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedElementNode,
|
||||
} from 'lexical';
|
||||
|
||||
import {addClassNamesToElement} from '@lexical/utils';
|
||||
@ -27,6 +28,13 @@ import {$isTableCellNode} from './LexicalTableCellNode';
|
||||
import {$isTableRowNode} from './LexicalTableRowNode';
|
||||
import {getTableGrid} from './LexicalTableSelectionHelpers';
|
||||
|
||||
export type SerializedTableNode = {
|
||||
...SerializedElementNode,
|
||||
type: 'table',
|
||||
version: 1,
|
||||
...
|
||||
};
|
||||
|
||||
export class TableNode extends GridNode {
|
||||
__grid: ?Grid;
|
||||
|
||||
@ -47,10 +55,22 @@ export class TableNode extends GridNode {
|
||||
};
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedTableNode): TableNode {
|
||||
return $createTableNode();
|
||||
}
|
||||
|
||||
constructor(key?: NodeKey): void {
|
||||
super(key);
|
||||
}
|
||||
|
||||
exportJSON(): SerializedElementNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'table',
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig, editor: LexicalEditor): HTMLElement {
|
||||
const tableElement = document.createElement('table');
|
||||
|
||||
|
@ -13,11 +13,20 @@ import type {
|
||||
EditorConfig,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedElementNode,
|
||||
} from 'lexical';
|
||||
|
||||
import {addClassNamesToElement} from '@lexical/utils';
|
||||
import {GridRowNode} from 'lexical';
|
||||
|
||||
export type SerializedTableRowNode = {
|
||||
...SerializedElementNode,
|
||||
height: number,
|
||||
type: 'tablerow',
|
||||
version: 1,
|
||||
...
|
||||
};
|
||||
|
||||
export class TableRowNode extends GridRowNode {
|
||||
__height: ?number;
|
||||
|
||||
@ -38,11 +47,23 @@ export class TableRowNode extends GridRowNode {
|
||||
};
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedTableRowNode): TableRowNode {
|
||||
return $createTableRowNode(serializedNode.height);
|
||||
}
|
||||
|
||||
constructor(height?: ?number, key?: NodeKey): void {
|
||||
super(key);
|
||||
this.__height = height;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedElementNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'tablerow',
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
const element = document.createElement('tr');
|
||||
|
||||
|
71
packages/lexical/Lexical.d.ts
vendored
71
packages/lexical/Lexical.d.ts
vendored
@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import {Class, $ReadOnly} from 'utility-types';
|
||||
import {Spread} from 'globals';
|
||||
|
||||
/**
|
||||
* LexicalCommands
|
||||
@ -161,6 +162,10 @@ export declare class LexicalEditor {
|
||||
parseEditorState(
|
||||
maybeStringifiedEditorState: string | ParsedEditorState,
|
||||
): EditorState;
|
||||
unstable_parseEditorState(
|
||||
maybeStringifiedEditorState: string | SerializedEditorState,
|
||||
updateFn?: () => void,
|
||||
): EditorState;
|
||||
update(updateFn: () => void, options?: EditorUpdateOptions): boolean;
|
||||
focus(callbackFn?: () => void): void;
|
||||
blur(): void;
|
||||
@ -291,6 +296,7 @@ export interface EditorState {
|
||||
isEmpty(): boolean;
|
||||
read<V>(callbackFn: () => V): V;
|
||||
toJSON(space?: string | number): JSONEditorState;
|
||||
unstable_toJSON(): SerializedEditorState;
|
||||
clone(
|
||||
selection?: RangeSelection | NodeSelection | GridSelection | null,
|
||||
): EditorState;
|
||||
@ -605,7 +611,10 @@ export declare class TextNode extends LexicalNode {
|
||||
toggleFormat(type: TextFormatType): TextNode;
|
||||
toggleDirectionless(): TextNode;
|
||||
toggleUnmergeable(): TextNode;
|
||||
setMode(type: TextModeType): TextNode;
|
||||
setMode(type: TextModeType): this;
|
||||
setDetail(detail: number): TextNode;
|
||||
getDetail(): number;
|
||||
getMode(): TextModeType;
|
||||
setTextContent(text: string): TextNode;
|
||||
select(_anchorOffset?: number, _focusOffset?: number): RangeSelection;
|
||||
spliceText(
|
||||
@ -619,6 +628,8 @@ export declare class TextNode extends LexicalNode {
|
||||
splitText(...splitOffsets: Array<number>): Array<TextNode>;
|
||||
mergeWithSibling(target: TextNode): TextNode;
|
||||
isTextEntity(): boolean;
|
||||
static importJSON(serializedTextNode: SerializedTextNode): TextNode;
|
||||
exportJSON(): SerializedTextNode;
|
||||
}
|
||||
export function $createTextNode(text?: string): TextNode;
|
||||
export function $isTextNode(
|
||||
@ -635,6 +646,10 @@ export declare class LineBreakNode extends LexicalNode {
|
||||
getTextContent(): '\n';
|
||||
createDOM(): HTMLElement;
|
||||
updateDOM(): false;
|
||||
static importJSON(
|
||||
serializedLineBreakNode: SerializedLexicalNode,
|
||||
): LineBreakNode;
|
||||
exportJSON(): SerializedLexicalNode;
|
||||
}
|
||||
export function $createLineBreakNode(): LineBreakNode;
|
||||
export function $isLineBreakNode(
|
||||
@ -658,6 +673,8 @@ export declare class RootNode extends ElementNode {
|
||||
updateDOM(prevNode: RootNode, dom: HTMLElement): false;
|
||||
append(...nodesToAppend: Array<LexicalNode>): ElementNode;
|
||||
canBeEmpty(): false;
|
||||
static importJSON(serializedRootNode: SerializedRootNode): RootNode;
|
||||
exportJSON(): SerializedElementNode;
|
||||
}
|
||||
export function $isRootNode(
|
||||
node: LexicalNode | null | undefined,
|
||||
@ -674,6 +691,7 @@ export declare class ElementNode extends LexicalNode {
|
||||
__dir: 'ltr' | 'rtl' | null;
|
||||
constructor(key?: NodeKey);
|
||||
getFormat(): number;
|
||||
getFormatType(): 'left' | 'center' | 'right' | 'justify';
|
||||
getIndent(): number;
|
||||
getChildren<T extends Array<LexicalNode>>(): T;
|
||||
getChildrenKeys(): Array<NodeKey>;
|
||||
@ -722,6 +740,7 @@ export declare class ElementNode extends LexicalNode {
|
||||
deleteCount: number,
|
||||
nodesToInsert: Array<LexicalNode>,
|
||||
): ElementNode;
|
||||
exportJSON(): SerializedElementNode;
|
||||
}
|
||||
export function $isElementNode(
|
||||
node: LexicalNode | null | undefined,
|
||||
@ -751,6 +770,10 @@ export declare class ParagraphNode extends ElementNode {
|
||||
updateDOM(prevNode: ParagraphNode, dom: HTMLElement): boolean;
|
||||
insertNewAfter(): ParagraphNode;
|
||||
collapseAtStart(): boolean;
|
||||
static importJSON(
|
||||
serializedParagraphNode: SerializedElementNode,
|
||||
): ParagraphNode;
|
||||
exportJSON(): SerializedElementNode;
|
||||
}
|
||||
export function $createParagraphNode(): ParagraphNode;
|
||||
export function $isParagraphNode(
|
||||
@ -797,3 +820,49 @@ export function $getDecoratorNode(
|
||||
* LexicalVersion
|
||||
*/
|
||||
export declare var VERSION: string;
|
||||
|
||||
// Serialization
|
||||
|
||||
export type SerializedLexicalNode = {
|
||||
type: string;
|
||||
version: number;
|
||||
};
|
||||
|
||||
export type SerializedTextNode = Spread<
|
||||
{
|
||||
detail: number;
|
||||
format: number;
|
||||
mode: TextModeType;
|
||||
style: string;
|
||||
text: string;
|
||||
},
|
||||
SerializedLexicalNode
|
||||
>;
|
||||
|
||||
export type SerializedElementNode = Spread<
|
||||
{
|
||||
children: Array<SerializedLexicalNode>;
|
||||
direction: 'ltr' | 'rtl' | null;
|
||||
format: 'left' | 'center' | 'right' | 'justify';
|
||||
indent: number;
|
||||
},
|
||||
SerializedLexicalNode
|
||||
>;
|
||||
|
||||
export type SerializedRootNode = Spread<
|
||||
{
|
||||
type: 'root';
|
||||
},
|
||||
SerializedElementNode
|
||||
>;
|
||||
|
||||
export type SerializedGridCellNode = Spread<
|
||||
{
|
||||
colSpan: number;
|
||||
},
|
||||
SerializedElementNode
|
||||
>;
|
||||
|
||||
export interface SerializedEditorState {
|
||||
root: SerializedRootNode;
|
||||
}
|
||||
|
@ -167,6 +167,10 @@ declare export class LexicalEditor {
|
||||
parseEditorState(
|
||||
maybeStringifiedEditorState: string | ParsedEditorState,
|
||||
): EditorState;
|
||||
unstable_parseEditorState<SerializedNode>(
|
||||
maybeStringifiedEditorState: string | SerializedEditorState<SerializedNode>,
|
||||
updateFn?: () => void,
|
||||
): EditorState;
|
||||
update(updateFn: () => void, options?: EditorUpdateOptions): boolean;
|
||||
focus(callbackFn?: () => void): void;
|
||||
blur(): void;
|
||||
@ -292,6 +296,7 @@ export interface EditorState {
|
||||
isEmpty(): boolean;
|
||||
read<V>(callbackFn: () => V): V;
|
||||
toJSON(space?: string | number): JSONEditorState;
|
||||
unstable_toJSON<SerializedNode>(): SerializedEditorState<SerializedNode>;
|
||||
clone(
|
||||
selection?: RangeSelection | NodeSelection | GridSelection | null,
|
||||
): EditorState;
|
||||
@ -631,6 +636,9 @@ declare export class TextNode extends LexicalNode {
|
||||
toggleDirectionless(): this;
|
||||
toggleUnmergeable(): this;
|
||||
setMode(type: TextModeType): this;
|
||||
setDetail(detail: number): this;
|
||||
getDetail(): number;
|
||||
getMode(): TextModeType;
|
||||
setTextContent(text: string): TextNode;
|
||||
select(_anchorOffset?: number, _focusOffset?: number): RangeSelection;
|
||||
spliceText(
|
||||
@ -644,6 +652,8 @@ declare export class TextNode extends LexicalNode {
|
||||
splitText(...splitOffsets: Array<number>): Array<TextNode>;
|
||||
mergeWithSibling(target: TextNode): TextNode;
|
||||
isTextEntity(): boolean;
|
||||
static importJSON(serializedTextNode: SerializedTextNode): TextNode;
|
||||
exportJSON(): SerializedTextNode;
|
||||
}
|
||||
declare export function $createTextNode(text?: string): TextNode;
|
||||
declare export function $isTextNode(
|
||||
@ -661,6 +671,10 @@ declare export class LineBreakNode extends LexicalNode {
|
||||
getTextContent(): '\n';
|
||||
createDOM(): HTMLElement;
|
||||
updateDOM(): false;
|
||||
static importJSON(
|
||||
serializedLineBreakNode: SerializedLineBreakNode,
|
||||
): LineBreakNode;
|
||||
exportJSON(): SerializedLexicalNode;
|
||||
}
|
||||
declare export function $createLineBreakNode(): LineBreakNode;
|
||||
declare export function $isLineBreakNode(
|
||||
@ -693,7 +707,7 @@ declare export function $isRootNode(
|
||||
/**
|
||||
* LexicalElementNode
|
||||
*/
|
||||
export type ElementFormatType = 'left' | 'center' | 'right' | 'justify';
|
||||
export type ElementFormatType = 'left' | 'center' | 'right' | 'justify' | '';
|
||||
declare export class ElementNode extends LexicalNode {
|
||||
__children: Array<NodeKey>;
|
||||
__format: number;
|
||||
@ -701,6 +715,7 @@ declare export class ElementNode extends LexicalNode {
|
||||
__dir: 'ltr' | 'rtl' | null;
|
||||
constructor(key?: NodeKey): void;
|
||||
getFormat(): number;
|
||||
getFormatType(): ElementFormatType;
|
||||
getIndent(): number;
|
||||
getChildren<T: Array<LexicalNode>>(): T;
|
||||
getChildrenKeys(): Array<NodeKey>;
|
||||
@ -749,6 +764,7 @@ declare export class ElementNode extends LexicalNode {
|
||||
deleteCount: number,
|
||||
nodesToInsert: Array<LexicalNode>,
|
||||
): ElementNode;
|
||||
exportJSON(): SerializedElementNode;
|
||||
}
|
||||
declare export function $isElementNode(
|
||||
node: ?LexicalNode,
|
||||
@ -779,6 +795,10 @@ declare export class ParagraphNode extends ElementNode {
|
||||
updateDOM(prevNode: ParagraphNode, dom: HTMLElement): boolean;
|
||||
insertNewAfter(): ParagraphNode;
|
||||
collapseAtStart(): boolean;
|
||||
static importJSON(
|
||||
serializedParagraphNode: SerializedParagraphNode,
|
||||
): ParagraphNode;
|
||||
exportJSON(): SerializedElementNode;
|
||||
}
|
||||
declare export function $createParagraphNode(): ParagraphNode;
|
||||
declare export function $isParagraphNode(
|
||||
@ -843,3 +863,58 @@ export type EventHandler = (
|
||||
* LexicalVersion
|
||||
*/
|
||||
declare export var VERSION: string;
|
||||
|
||||
// Serialization
|
||||
|
||||
export type SerializedLexicalNode = {
|
||||
type: string,
|
||||
version: number,
|
||||
...
|
||||
};
|
||||
|
||||
export type SerializedTextNode = {
|
||||
...SerializedLexicalNode,
|
||||
detail: number,
|
||||
format: number,
|
||||
mode: TextModeType,
|
||||
style: string,
|
||||
text: string,
|
||||
...
|
||||
};
|
||||
|
||||
export type SerializedElementNode = {
|
||||
...SerializedLexicalNode,
|
||||
children: Array<SerializedLexicalNode>,
|
||||
direction: 'ltr' | 'rtl' | null,
|
||||
format: ElementFormatType,
|
||||
indent: number,
|
||||
...
|
||||
};
|
||||
|
||||
export type SerializedParagraphNode = {
|
||||
...SerializedElementNode,
|
||||
type: 'paragraph',
|
||||
...
|
||||
};
|
||||
|
||||
export type SerializedLineBreakNode = {
|
||||
...SerializedLexicalNode,
|
||||
type: 'linebreak',
|
||||
...
|
||||
};
|
||||
|
||||
export type SerializedRootNode = {
|
||||
...SerializedElementNode,
|
||||
type: 'root',
|
||||
...
|
||||
};
|
||||
|
||||
export type SerializedGridCellNode = {
|
||||
...SerializedElementNode,
|
||||
colSpan: number,
|
||||
...
|
||||
};
|
||||
|
||||
export interface SerializedEditorState<SerializedNode> {
|
||||
root: SerializedRootNode<SerializedNode>;
|
||||
}
|
||||
|
@ -85,9 +85,23 @@ export const ELEMENT_TYPE_TO_FORMAT: {[ElementFormatType]: number} = {
|
||||
right: IS_ALIGN_RIGHT,
|
||||
};
|
||||
|
||||
export const ELEMENT_FORMAT_TO_TYPE: {[number]: ElementFormatType} = {
|
||||
[IS_ALIGN_CENTER]: 'center',
|
||||
[IS_ALIGN_JUSTIFY]: 'justify',
|
||||
[IS_ALIGN_LEFT]: 'left',
|
||||
[IS_ALIGN_RIGHT]: 'right',
|
||||
};
|
||||
|
||||
export const TEXT_MODE_TO_TYPE: {[TextModeType]: 0 | 1 | 2 | 3} = {
|
||||
inert: IS_INERT,
|
||||
normal: IS_NORMAL,
|
||||
segmented: IS_SEGMENTED,
|
||||
token: IS_TOKEN,
|
||||
};
|
||||
|
||||
export const TEXT_TYPE_TO_MODE: {[number]: TextModeType} = {
|
||||
[IS_INERT]: 'inert',
|
||||
[IS_NORMAL]: 'normal',
|
||||
[IS_SEGMENTED]: 'segmented',
|
||||
[IS_TOKEN]: 'token',
|
||||
};
|
||||
|
@ -7,7 +7,11 @@
|
||||
* @flow strict
|
||||
*/
|
||||
|
||||
import type {EditorState, ParsedEditorState} from './LexicalEditorState';
|
||||
import type {
|
||||
EditorState,
|
||||
ParsedEditorState,
|
||||
SerializedEditorState,
|
||||
} from './LexicalEditorState';
|
||||
import type {DOMConversion, LexicalNode, NodeKey} from './LexicalNode';
|
||||
|
||||
import getDOMSelection from 'shared/getDOMSelection';
|
||||
@ -22,6 +26,7 @@ import {
|
||||
commitPendingUpdates,
|
||||
parseEditorState,
|
||||
triggerListeners,
|
||||
unstable_parseEditorState,
|
||||
updateEditor,
|
||||
} from './LexicalUpdates';
|
||||
import {
|
||||
@ -575,6 +580,7 @@ export class LexicalEditor {
|
||||
}
|
||||
commitPendingUpdates(this);
|
||||
}
|
||||
// TODO: once unstable_parseEditorState is stable, swap that for this.
|
||||
parseEditorState(
|
||||
maybeStringifiedEditorState: string | ParsedEditorState,
|
||||
): EditorState {
|
||||
@ -584,6 +590,16 @@ export class LexicalEditor {
|
||||
: maybeStringifiedEditorState;
|
||||
return parseEditorState(parsedEditorState, this);
|
||||
}
|
||||
unstable_parseEditorState(
|
||||
maybeStringifiedEditorState: string | SerializedEditorState,
|
||||
updateFn?: () => void,
|
||||
): EditorState {
|
||||
const serializedEditorState =
|
||||
typeof maybeStringifiedEditorState === 'string'
|
||||
? JSON.parse(maybeStringifiedEditorState)
|
||||
: maybeStringifiedEditorState;
|
||||
return unstable_parseEditorState(serializedEditorState, this, updateFn);
|
||||
}
|
||||
update(updateFn: () => void, options?: EditorUpdateOptions): void {
|
||||
updateEditor(this, updateFn, options);
|
||||
}
|
||||
|
@ -15,25 +15,34 @@ import type {
|
||||
NodeSelection,
|
||||
RangeSelection,
|
||||
} from './LexicalSelection';
|
||||
import type {SerializedRootNode} from './nodes/LexicalRootNode';
|
||||
|
||||
import invariant from '../../shared/src/invariant';
|
||||
import {$isElementNode} from '.';
|
||||
import {
|
||||
$isGridSelection,
|
||||
$isNodeSelection,
|
||||
$isRangeSelection,
|
||||
} from './LexicalSelection';
|
||||
import {readEditorState} from './LexicalUpdates';
|
||||
import {$getRoot} from './LexicalUtils';
|
||||
import {$createRootNode} from './nodes/LexicalRootNode';
|
||||
|
||||
// TODO: deprecated
|
||||
export type ParsedEditorState = {
|
||||
_nodeMap: Array<[NodeKey, ParsedNode]>,
|
||||
_selection: null | ParsedSelection,
|
||||
};
|
||||
|
||||
// TODO: deprecated
|
||||
export type JSONEditorState = {
|
||||
_nodeMap: Array<[NodeKey, LexicalNode]>,
|
||||
_selection: null | ParsedSelection,
|
||||
};
|
||||
|
||||
export interface SerializedEditorState {
|
||||
root: SerializedRootNode;
|
||||
}
|
||||
|
||||
export function editorStateHasDirtySelection(
|
||||
editorState: EditorState,
|
||||
editor: LexicalEditor,
|
||||
@ -59,6 +68,35 @@ export function createEmptyEditorState(): EditorState {
|
||||
return new EditorState(new Map([['root', $createRootNode()]]));
|
||||
}
|
||||
|
||||
function exportNodeToJSON<SerializedNode>(node: LexicalNode): SerializedNode {
|
||||
const serializedNode = node.exportJSON();
|
||||
const nodeClass = node.constructor;
|
||||
if (serializedNode.type !== nodeClass.getType()) {
|
||||
invariant(
|
||||
false,
|
||||
'LexicalNode: Node %s does not implement .exportJSON().',
|
||||
nodeClass.name,
|
||||
);
|
||||
}
|
||||
const serializedChildren = serializedNode.children;
|
||||
if ($isElementNode(node)) {
|
||||
if (!Array.isArray(serializedChildren)) {
|
||||
invariant(
|
||||
false,
|
||||
'LexicalNode: Node %s is an element but .exportJSON() does not have a children array.',
|
||||
nodeClass.name,
|
||||
);
|
||||
}
|
||||
const children = node.getChildren();
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i];
|
||||
const serializedChildNode = exportNodeToJSON(child);
|
||||
serializedChildren.push(serializedChildNode);
|
||||
}
|
||||
}
|
||||
return serializedNode;
|
||||
}
|
||||
|
||||
export class EditorState {
|
||||
_nodeMap: NodeMap;
|
||||
_selection: null | RangeSelection | NodeSelection | GridSelection;
|
||||
@ -90,6 +128,7 @@ export class EditorState {
|
||||
editorState._readOnly = true;
|
||||
return editorState;
|
||||
}
|
||||
// TODO: remove when we use the other toJSON
|
||||
toJSON(space?: string | number): JSONEditorState {
|
||||
const selection = this._selection;
|
||||
|
||||
@ -132,4 +171,9 @@ export class EditorState {
|
||||
: null,
|
||||
};
|
||||
}
|
||||
unstable_toJSON(): SerializedEditorState {
|
||||
return readEditorState(this, () => ({
|
||||
root: exportNodeToJSON($getRoot()),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -43,6 +43,12 @@ import {
|
||||
|
||||
export type NodeMap = Map<NodeKey, LexicalNode>;
|
||||
|
||||
export type SerializedLexicalNode = {
|
||||
type: string,
|
||||
version: number,
|
||||
...
|
||||
};
|
||||
|
||||
export function removeNode(
|
||||
nodeToRemove: LexicalNode,
|
||||
restoreSelection: boolean,
|
||||
@ -600,6 +606,22 @@ export class LexicalNode {
|
||||
return null;
|
||||
}
|
||||
|
||||
// $FlowFixMe: Intentional work around for Flow
|
||||
exportJSON(): Object {
|
||||
invariant(false, 'exportJSON: base method not extended');
|
||||
}
|
||||
|
||||
static importJSON(
|
||||
// $FlowFixMe: Intentional work around for Flow
|
||||
serializedNode: Object,
|
||||
): LexicalNode {
|
||||
invariant(
|
||||
false,
|
||||
'LexicalNode: Node %s does not implement .importJSON().',
|
||||
this.name,
|
||||
);
|
||||
}
|
||||
|
||||
// Setters and mutators
|
||||
|
||||
remove(preserveEmptyParent?: boolean): void {
|
||||
|
@ -12,19 +12,24 @@ import type {
|
||||
LexicalCommand,
|
||||
LexicalEditor,
|
||||
MutatedNodes,
|
||||
RegisteredNodes,
|
||||
Transform,
|
||||
} from './LexicalEditor';
|
||||
import type {ParsedEditorState} from './LexicalEditorState';
|
||||
import type {
|
||||
ParsedEditorState,
|
||||
SerializedEditorState,
|
||||
} from './LexicalEditorState';
|
||||
import type {LexicalNode} from './LexicalNode';
|
||||
import type {NodeParserState, ParsedNode} from './LexicalParsing';
|
||||
|
||||
import invariant from 'shared/invariant';
|
||||
|
||||
import {$isTextNode} from '.';
|
||||
import {$isElementNode, $isTextNode} from '.';
|
||||
import {FULL_RECONCILE, NO_DIRTY_NODES} from './LexicalConstants';
|
||||
import {resetEditor} from './LexicalEditor';
|
||||
import {
|
||||
cloneEditorState,
|
||||
createEmptyEditorState,
|
||||
EditorState,
|
||||
editorStateHasDirtySelection,
|
||||
} from './LexicalEditorState';
|
||||
@ -242,6 +247,7 @@ function $applyAllTransforms(
|
||||
editor._dirtyElements = dirtyElements;
|
||||
}
|
||||
|
||||
// TODO: once unstable_parseEditorState is stable, swap that for this.
|
||||
export function parseEditorState(
|
||||
parsedEditorState: ParsedEditorState,
|
||||
editor: LexicalEditor,
|
||||
@ -279,6 +285,77 @@ export function parseEditorState(
|
||||
return editorState;
|
||||
}
|
||||
|
||||
type InternalSerializedNode = {
|
||||
children?: Array<InternalSerializedNode>,
|
||||
type: string,
|
||||
version: number,
|
||||
};
|
||||
|
||||
function parseSerializedNode<SerializedNode: InternalSerializedNode>(
|
||||
serializedNode: SerializedNode,
|
||||
registeredNodes: RegisteredNodes,
|
||||
): LexicalNode {
|
||||
const type = serializedNode.type;
|
||||
const registeredNode = registeredNodes.get(type);
|
||||
if (registeredNode === undefined) {
|
||||
invariant(false, 'parseEditorState: type "%s" + not found', type);
|
||||
}
|
||||
const nodeClass = registeredNode.klass;
|
||||
if (serializedNode.type !== nodeClass.getType()) {
|
||||
invariant(
|
||||
false,
|
||||
'LexicalNode: Node %s does not implement .importJSON().',
|
||||
nodeClass.name,
|
||||
);
|
||||
}
|
||||
const node = nodeClass.importJSON(serializedNode);
|
||||
const children = serializedNode.children;
|
||||
if ($isElementNode(node) && Array.isArray(children)) {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const serializedJSONChildNode = children[i];
|
||||
const childNode = parseSerializedNode(
|
||||
serializedJSONChildNode,
|
||||
registeredNodes,
|
||||
);
|
||||
node.append(childNode);
|
||||
}
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
export function unstable_parseEditorState(
|
||||
serializedEditorState: SerializedEditorState,
|
||||
editor: LexicalEditor,
|
||||
updateFn: void | (() => void),
|
||||
): EditorState {
|
||||
const editorState = createEmptyEditorState();
|
||||
const previousActiveEditorState = activeEditorState;
|
||||
const previousReadOnlyMode = isReadOnlyMode;
|
||||
const previousActiveEditor = activeEditor;
|
||||
activeEditorState = editorState;
|
||||
isReadOnlyMode = false;
|
||||
activeEditor = editor;
|
||||
try {
|
||||
const registeredNodes = editor._nodes;
|
||||
// $FlowFixMe: intentional cast to our internal type
|
||||
const serializedNode: InternalSerializedNode = serializedEditorState.root;
|
||||
parseSerializedNode(serializedNode, registeredNodes);
|
||||
if (updateFn) {
|
||||
updateFn();
|
||||
}
|
||||
// Make the editorState immutable
|
||||
editorState._readOnly = true;
|
||||
if (__DEV__) {
|
||||
handleDEVOnlyPendingUpdateGuarantees(editorState);
|
||||
}
|
||||
} finally {
|
||||
activeEditorState = previousActiveEditorState;
|
||||
isReadOnlyMode = previousReadOnlyMode;
|
||||
activeEditor = previousActiveEditor;
|
||||
}
|
||||
return editorState;
|
||||
}
|
||||
|
||||
// This technically isn't an update but given we need
|
||||
// exposure to the module's active bindings, we have this
|
||||
// function here
|
||||
|
@ -1006,6 +1006,26 @@ describe('LexicalEditor tests', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('exportJSON API - parses parsed JSON', async () => {
|
||||
await update(() => {
|
||||
const paragraph = $createParagraphNode();
|
||||
originalText = $createTextNode('Hello world');
|
||||
originalText.select(6, 11);
|
||||
paragraph.append(originalText);
|
||||
$getRoot().append(paragraph);
|
||||
});
|
||||
const stringifiedEditorState = JSON.stringify(
|
||||
editor.getEditorState().unstable_toJSON(),
|
||||
);
|
||||
const parsedEditorStateFromObject = editor.unstable_parseEditorState(
|
||||
JSON.parse(stringifiedEditorState),
|
||||
);
|
||||
parsedEditorStateFromObject.read(() => {
|
||||
const root = $getRoot();
|
||||
expect(root.getTextContent()).toMatch(/Hello world/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('range selection', () => {
|
||||
beforeEach(async () => {
|
||||
init();
|
||||
|
@ -6,7 +6,7 @@
|
||||
*
|
||||
* @flow strict
|
||||
*/
|
||||
import type {NodeKey} from '../LexicalNode';
|
||||
import type {NodeKey, SerializedLexicalNode} from '../LexicalNode';
|
||||
import type {
|
||||
GridSelection,
|
||||
NodeSelection,
|
||||
@ -17,7 +17,11 @@ import type {
|
||||
import invariant from 'shared/invariant';
|
||||
|
||||
import {$isRootNode, $isTextNode, TextNode} from '../';
|
||||
import {DOUBLE_LINE_BREAK, ELEMENT_TYPE_TO_FORMAT} from '../LexicalConstants';
|
||||
import {
|
||||
DOUBLE_LINE_BREAK,
|
||||
ELEMENT_FORMAT_TO_TYPE,
|
||||
ELEMENT_TYPE_TO_FORMAT,
|
||||
} from '../LexicalConstants';
|
||||
import {LexicalNode} from '../LexicalNode';
|
||||
import {
|
||||
$getSelection,
|
||||
@ -32,7 +36,16 @@ import {
|
||||
removeFromParent,
|
||||
} from '../LexicalUtils';
|
||||
|
||||
export type ElementFormatType = 'left' | 'center' | 'right' | 'justify';
|
||||
export type SerializedElementNode = {
|
||||
...SerializedLexicalNode,
|
||||
children: Array<SerializedLexicalNode>,
|
||||
direction: 'ltr' | 'rtl' | null,
|
||||
format: 'left' | 'center' | 'right' | 'justify' | '',
|
||||
indent: number,
|
||||
...
|
||||
};
|
||||
|
||||
export type ElementFormatType = 'left' | 'center' | 'right' | 'justify' | '';
|
||||
|
||||
export class ElementNode extends LexicalNode {
|
||||
__children: Array<NodeKey>;
|
||||
@ -52,6 +65,10 @@ export class ElementNode extends LexicalNode {
|
||||
const self = this.getLatest();
|
||||
return self.__format;
|
||||
}
|
||||
getFormatType(): 'left' | 'center' | 'right' | 'justify' | '' {
|
||||
const format = this.getFormat();
|
||||
return ELEMENT_FORMAT_TO_TYPE[format] || '';
|
||||
}
|
||||
getIndent(): number {
|
||||
const self = this.getLatest();
|
||||
return self.__indent;
|
||||
@ -283,7 +300,7 @@ export class ElementNode extends LexicalNode {
|
||||
setFormat(type: ElementFormatType): this {
|
||||
errorOnReadOnly();
|
||||
const self = this.getWritable();
|
||||
self.__format = ELEMENT_TYPE_TO_FORMAT[type];
|
||||
self.__format = ELEMENT_TYPE_TO_FORMAT[type] || 0;
|
||||
return this;
|
||||
}
|
||||
setIndent(indentLevel: number): this {
|
||||
@ -410,6 +427,17 @@ export class ElementNode extends LexicalNode {
|
||||
|
||||
return writableSelf;
|
||||
}
|
||||
// JSON serialization
|
||||
exportJSON(): SerializedElementNode {
|
||||
return {
|
||||
children: [],
|
||||
direction: this.getDirection(),
|
||||
format: this.getFormatType(),
|
||||
indent: this.getIndent(),
|
||||
type: 'element',
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
// These are intended to be extends for specific element heuristics.
|
||||
insertNewAfter(selection: RangeSelection): null | LexicalNode {
|
||||
return null;
|
||||
|
@ -7,16 +7,29 @@
|
||||
* @flow strict
|
||||
*/
|
||||
|
||||
import type {LexicalNode, NodeKey} from 'lexical';
|
||||
import type {LexicalNode, NodeKey, SerializedElementNode} from 'lexical';
|
||||
|
||||
import {ElementNode} from './LexicalElementNode';
|
||||
|
||||
export type SerializedGridCellNode = {
|
||||
...SerializedElementNode,
|
||||
colSpan: number,
|
||||
...
|
||||
};
|
||||
|
||||
export class GridCellNode extends ElementNode {
|
||||
__colSpan: number;
|
||||
|
||||
constructor(colSpan: number, key?: NodeKey) {
|
||||
super(key);
|
||||
}
|
||||
|
||||
exportJSON(): SerializedElementNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
colSpan: this.__colSpan,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function $isGridCellNode(node: ?LexicalNode): boolean %checks {
|
||||
|
@ -11,10 +11,17 @@ import type {
|
||||
DOMConversionMap,
|
||||
DOMConversionOutput,
|
||||
NodeKey,
|
||||
SerializedLexicalNode,
|
||||
} from '../LexicalNode';
|
||||
|
||||
import {LexicalNode} from '../LexicalNode';
|
||||
|
||||
export type SerializedLineBreakNode = {
|
||||
...SerializedLexicalNode,
|
||||
type: 'linebreak',
|
||||
...
|
||||
};
|
||||
|
||||
export class LineBreakNode extends LexicalNode {
|
||||
static getType(): string {
|
||||
return 'linebreak';
|
||||
@ -48,6 +55,19 @@ export class LineBreakNode extends LexicalNode {
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
static importJSON(
|
||||
serializedLineBreakNode: SerializedLineBreakNode,
|
||||
): LineBreakNode {
|
||||
return $createLineBreakNode();
|
||||
}
|
||||
|
||||
exportJSON(): SerializedLexicalNode {
|
||||
return {
|
||||
type: 'linebreak',
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function convertLineBreakElement(node: Node): DOMConversionOutput {
|
||||
|
@ -17,11 +17,19 @@ import type {
|
||||
DOMExportOutput,
|
||||
LexicalNode,
|
||||
} from '../LexicalNode';
|
||||
import type {SerializedElementNode} from './LexicalElementNode';
|
||||
|
||||
import {getCachedClassNameArray} from '../LexicalUtils';
|
||||
import {ElementNode} from './LexicalElementNode';
|
||||
import {$isTextNode} from './LexicalTextNode';
|
||||
|
||||
export type SerializedParagraphNode = {
|
||||
...SerializedElementNode,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
...
|
||||
};
|
||||
|
||||
export class ParagraphNode extends ElementNode {
|
||||
static getType(): string {
|
||||
return 'paragraph';
|
||||
@ -72,6 +80,22 @@ export class ParagraphNode extends ElementNode {
|
||||
};
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedParagraphNode): ParagraphNode {
|
||||
const node = $createParagraphNode();
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setIndent(serializedNode.indent);
|
||||
node.setDirection(serializedNode.direction);
|
||||
return node;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedElementNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
// Mutation
|
||||
|
||||
insertNewAfter(): ParagraphNode {
|
||||
|
@ -9,14 +9,21 @@
|
||||
|
||||
import type {LexicalNode} from '../LexicalNode';
|
||||
import type {ParsedElementNode} from '../LexicalParsing';
|
||||
import type {SerializedElementNode} from './LexicalElementNode';
|
||||
|
||||
import invariant from 'shared/invariant';
|
||||
|
||||
import {NO_DIRTY_NODES} from '../LexicalConstants';
|
||||
import {getActiveEditor, isCurrentlyReadOnlyMode} from '../LexicalUpdates';
|
||||
import {$getRoot} from '../LexicalUtils';
|
||||
import {$isDecoratorNode} from './LexicalDecoratorNode';
|
||||
import {$isElementNode, ElementNode} from './LexicalElementNode';
|
||||
|
||||
export type SerializedRootNode = {
|
||||
...SerializedElementNode,
|
||||
...
|
||||
};
|
||||
|
||||
export class RootNode extends ElementNode {
|
||||
__cachedText: null | string;
|
||||
|
||||
@ -93,6 +100,26 @@ export class RootNode extends ElementNode {
|
||||
return super.append(...nodesToAppend);
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedRootNode): RootNode {
|
||||
// We don't create a root, and instead use the existing root.
|
||||
const node = $getRoot();
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setIndent(serializedNode.indent);
|
||||
node.setDirection(serializedNode.direction);
|
||||
return node;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedRootNode {
|
||||
return {
|
||||
children: [],
|
||||
direction: this.getDirection(),
|
||||
format: this.getFormatType(),
|
||||
indent: this.getIndent(),
|
||||
type: 'root',
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
// TODO: Deprecated
|
||||
toJSON(): ParsedElementNode {
|
||||
return {
|
||||
__children: this.__children,
|
||||
|
@ -12,6 +12,7 @@ import type {
|
||||
DOMConversionMap,
|
||||
DOMConversionOutput,
|
||||
NodeKey,
|
||||
SerializedLexicalNode,
|
||||
} from '../LexicalNode';
|
||||
import type {
|
||||
GridSelection,
|
||||
@ -37,6 +38,7 @@ import {
|
||||
IS_UNMERGEABLE,
|
||||
TEXT_MODE_TO_TYPE,
|
||||
TEXT_TYPE_TO_FORMAT,
|
||||
TEXT_TYPE_TO_MODE,
|
||||
} from '../LexicalConstants';
|
||||
import {LexicalNode} from '../LexicalNode';
|
||||
import {
|
||||
@ -55,6 +57,16 @@ import {
|
||||
toggleTextFormatType,
|
||||
} from '../LexicalUtils';
|
||||
|
||||
export type SerializedTextNode = {
|
||||
...SerializedLexicalNode,
|
||||
detail: number,
|
||||
format: number,
|
||||
mode: TextModeType,
|
||||
style: string,
|
||||
text: string,
|
||||
...
|
||||
};
|
||||
|
||||
export type TextFormatType =
|
||||
| 'bold'
|
||||
| 'underline'
|
||||
@ -264,6 +276,16 @@ export class TextNode extends LexicalNode {
|
||||
return self.__format;
|
||||
}
|
||||
|
||||
getDetail(): number {
|
||||
const self = this.getLatest();
|
||||
return self.__detail;
|
||||
}
|
||||
|
||||
getMode(): TextModeType {
|
||||
const self = this.getLatest();
|
||||
return TEXT_TYPE_TO_MODE[self.__mode];
|
||||
}
|
||||
|
||||
getStyle(): string {
|
||||
const self = this.getLatest();
|
||||
return self.__style;
|
||||
@ -447,6 +469,27 @@ export class TextNode extends LexicalNode {
|
||||
};
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedTextNode): TextNode {
|
||||
const node = $createTextNode(serializedNode.text);
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setDetail(serializedNode.detail);
|
||||
node.setMode(serializedNode.mode);
|
||||
node.setStyle(serializedNode.style);
|
||||
return node;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedTextNode {
|
||||
return {
|
||||
detail: this.getDetail(),
|
||||
format: this.getFormat(),
|
||||
mode: this.getMode(),
|
||||
style: this.getStyle(),
|
||||
text: this.getTextContent(),
|
||||
type: 'text',
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
// Mutators
|
||||
selectionTransform(
|
||||
prevSelection: null | RangeSelection | NodeSelection | GridSelection,
|
||||
@ -460,6 +503,13 @@ export class TextNode extends LexicalNode {
|
||||
return self;
|
||||
}
|
||||
|
||||
setDetail(detail: number): this {
|
||||
errorOnReadOnly();
|
||||
const self = this.getWritable();
|
||||
self.__detail = detail;
|
||||
return self;
|
||||
}
|
||||
|
||||
setStyle(style: string): this {
|
||||
errorOnReadOnly();
|
||||
const self = this.getWritable();
|
||||
|
@ -82,6 +82,26 @@ describe('LexicalElementNode tests', () => {
|
||||
});
|
||||
}
|
||||
|
||||
describe('exportJSON()', () => {
|
||||
test('should return and object conforming to the expected schema', async () => {
|
||||
await update(() => {
|
||||
const node = $createTestElementNode();
|
||||
// If you broke this test, you changed the public interface of a
|
||||
// serialized Lexical Core Node. Please ensure the correct adapter
|
||||
// logic is in place in the corresponding importJSON method
|
||||
// to accomodate these changes.
|
||||
expect(node.exportJSON()).toStrictEqual({
|
||||
children: [],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'element',
|
||||
version: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getChildren()', () => {
|
||||
test('no children', async () => {
|
||||
await update(() => {
|
||||
|
@ -25,6 +25,21 @@ describe('LexicalLineBreakNode tests', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('LineBreakNode.exportJSON() should return and object conforming to the expected schema', async () => {
|
||||
const {editor} = testEnv;
|
||||
await editor.update(() => {
|
||||
const node = $createLineBreakNode();
|
||||
// If you broke this test, you changed the public interface of a
|
||||
// serialized Lexical Core Node. Please ensure the correct adapter
|
||||
// logic is in place in the corresponding importJSON method
|
||||
// to accomodate these changes.
|
||||
expect(node.exportJSON()).toStrictEqual({
|
||||
type: 'linebreak',
|
||||
version: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('LineBreakNode.createDOM()', async () => {
|
||||
const {editor} = testEnv;
|
||||
await editor.update(() => {
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
ParagraphNode,
|
||||
} from 'lexical';
|
||||
|
||||
import {initializeUnitTest} from '../../__tests__/utils';
|
||||
import {initializeUnitTest} from '../../../__tests__/utils';
|
||||
|
||||
const editorConfig = Object.freeze({
|
||||
theme: {
|
||||
@ -37,6 +37,25 @@ describe('LexicalParagraphNode tests', () => {
|
||||
expect(() => new ParagraphNode()).toThrow();
|
||||
});
|
||||
|
||||
test('ParagraphNode.exportJSON() should return and object conforming to the expected schema', async () => {
|
||||
const {editor} = testEnv;
|
||||
await editor.update(() => {
|
||||
const node = $createParagraphNode();
|
||||
// If you broke this test, you changed the public interface of a
|
||||
// serialized Lexical Core Node. Please ensure the correct adapter
|
||||
// logic is in place in the corresponding importJSON method
|
||||
// to accomodate these changes.
|
||||
expect(node.exportJSON()).toStrictEqual({
|
||||
children: [],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('ParagraphNode.createDOM()', async () => {
|
||||
const {editor} = testEnv;
|
||||
await editor.update(() => {
|
@ -58,6 +58,25 @@ describe('LexicalRootNode tests', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('RootNode.exportJSON() should return and object conforming to the expected schema', async () => {
|
||||
const {editor} = testEnv;
|
||||
await editor.update(() => {
|
||||
const node = $createRootNode();
|
||||
// If you broke this test, you changed the public interface of a
|
||||
// serialized Lexical Core Node. Please ensure the correct adapter
|
||||
// logic is in place in the corresponding importJSON method
|
||||
// to accomodate these changes.
|
||||
expect(node.exportJSON()).toStrictEqual({
|
||||
children: [],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'root',
|
||||
version: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('RootNode.clone()', async () => {
|
||||
const rootNodeClone = rootNode.constructor.clone();
|
||||
expect(rootNodeClone).not.toBe(rootNode);
|
||||
|
@ -108,6 +108,27 @@ describe('LexicalTextNode tests', () => {
|
||||
});
|
||||
}
|
||||
|
||||
describe('exportJSON()', () => {
|
||||
test('should return and object conforming to the expected schema', async () => {
|
||||
await update(() => {
|
||||
const node = $createTextNode();
|
||||
// If you broke this test, you changed the public interface of a
|
||||
// serialized Lexical Core Node. Please ensure the correct adapter
|
||||
// logic is in place in the corresponding importJSON method
|
||||
// to accomodate these changes.
|
||||
expect(node.exportJSON()).toStrictEqual({
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: '',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('root.getTextContent()', () => {
|
||||
test('writable nodes', async () => {
|
||||
let nodeKey;
|
||||
|
@ -191,7 +191,7 @@ async function build(name, inputFile, outputFile, isProd) {
|
||||
{
|
||||
transform(source) {
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
extractCodes && findAndRecordErrorCodes(source);
|
||||
extractCodes && findAndRecordErrorCodes(source, isTypeScript);
|
||||
return source;
|
||||
},
|
||||
},
|
||||
|
@ -68,5 +68,11 @@
|
||||
"66": "createNode: node does not exist in nodeMap",
|
||||
"67": "reconcileNode: prevNode or nextNode does not exist in nodeMap",
|
||||
"68": "reconcileNode: parentDOM is null",
|
||||
"69": "Reconciliation: could not find DOM element for node key \"${key}\""
|
||||
"69": "Reconciliation: could not find DOM element for node key \"${key}\"",
|
||||
"70": "Incorrect node type received in importJSON for %s",
|
||||
"71": "LexicalNode: Node %s does not implement .exportJSON().",
|
||||
"72": "LexicalNode: Node %s is an element but .exportJSON() does not have a children array.",
|
||||
"73": "parseEditorState: type \"%s\" + not found",
|
||||
"74": "LexicalNode: Node %s does not implement .importJSON().",
|
||||
"75": "exportJSON: base method not extended"
|
||||
}
|
||||
|
@ -13,19 +13,19 @@ const traverse = require('@babel/traverse').default;
|
||||
const evalToString = require('./evalToString');
|
||||
const invertObject = require('./invertObject');
|
||||
|
||||
const plugins = [
|
||||
'classProperties',
|
||||
'jsx',
|
||||
'trailingFunctionCommas',
|
||||
'objectRestSpread',
|
||||
];
|
||||
|
||||
const babylonOptions = {
|
||||
// As a parser, babylon has its own options and we can't directly
|
||||
// import/require a babel preset. It should be kept **the same** as
|
||||
// the `babel-plugin-syntax-*` ones specified in
|
||||
// https://github.com/facebook/fbjs/blob/master/packages/babel-preset-fbjs/configure.js
|
||||
plugins: [
|
||||
'classProperties',
|
||||
'flow',
|
||||
'jsx',
|
||||
'trailingFunctionCommas',
|
||||
'objectRestSpread',
|
||||
],
|
||||
|
||||
plugins,
|
||||
sourceType: 'module',
|
||||
};
|
||||
|
||||
@ -99,7 +99,13 @@ module.exports = function (opts) {
|
||||
);
|
||||
}
|
||||
|
||||
return function extractErrors(source) {
|
||||
return function extractErrors(source, isTypeScript) {
|
||||
if (isTypeScript) {
|
||||
babylonOptions.plugins = [...plugins, 'typescript'];
|
||||
} else {
|
||||
babylonOptions.plugins = [...plugins, 'flow'];
|
||||
}
|
||||
|
||||
transform(source);
|
||||
flush();
|
||||
};
|
||||
|
Reference in New Issue
Block a user