mirror of
https://github.com/facebook/lexical.git
synced 2025-08-06 16:39:33 +08:00
[Breaking Change][lexical] Feature: Add updateFromJSON and move more textFormat/textStyle to ElementNode (#6970)
This commit is contained in:
@ -10,6 +10,7 @@ import type {
|
||||
EditorConfig,
|
||||
EditorThemeClasses,
|
||||
LexicalNode,
|
||||
LexicalUpdateJSON,
|
||||
LineBreakNode,
|
||||
NodeKey,
|
||||
SerializedTextNode,
|
||||
@ -97,7 +98,7 @@ export class CodeHighlightNode extends TextNode {
|
||||
__highlightType: string | null | undefined;
|
||||
|
||||
constructor(
|
||||
text: string,
|
||||
text: string = '',
|
||||
highlightType?: string | null | undefined,
|
||||
key?: NodeKey,
|
||||
) {
|
||||
@ -122,6 +123,12 @@ export class CodeHighlightNode extends TextNode {
|
||||
return self.__highlightType;
|
||||
}
|
||||
|
||||
setHighlightType(highlightType?: string | null | undefined): this {
|
||||
const self = this.getWritable();
|
||||
self.__highlightType = highlightType || undefined;
|
||||
return self;
|
||||
}
|
||||
|
||||
canHaveFormat(): boolean {
|
||||
return false;
|
||||
}
|
||||
@ -160,15 +167,15 @@ export class CodeHighlightNode extends TextNode {
|
||||
static importJSON(
|
||||
serializedNode: SerializedCodeHighlightNode,
|
||||
): CodeHighlightNode {
|
||||
const node = $createCodeHighlightNode(
|
||||
serializedNode.text,
|
||||
serializedNode.highlightType,
|
||||
);
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setDetail(serializedNode.detail);
|
||||
node.setMode(serializedNode.mode);
|
||||
node.setStyle(serializedNode.style);
|
||||
return node;
|
||||
return $createCodeHighlightNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
updateFromJSON(
|
||||
serializedNode: LexicalUpdateJSON<SerializedCodeHighlightNode>,
|
||||
): this {
|
||||
return super
|
||||
.updateFromJSON(serializedNode)
|
||||
.setHighlightType(serializedNode.highlightType);
|
||||
}
|
||||
|
||||
exportJSON(): SerializedCodeHighlightNode {
|
||||
@ -205,7 +212,7 @@ function getHighlightThemeClass(
|
||||
}
|
||||
|
||||
export function $createCodeHighlightNode(
|
||||
text: string,
|
||||
text: string = '',
|
||||
highlightType?: string | null | undefined,
|
||||
): CodeHighlightNode {
|
||||
return $applyNodeReplacement(new CodeHighlightNode(text, highlightType));
|
||||
|
@ -14,6 +14,7 @@ import type {
|
||||
EditorConfig,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
LexicalUpdateJSON,
|
||||
NodeKey,
|
||||
ParagraphNode,
|
||||
RangeSelection,
|
||||
@ -88,7 +89,7 @@ export class CodeNode extends ElementNode {
|
||||
|
||||
constructor(language?: string | null | undefined, key?: NodeKey) {
|
||||
super(key);
|
||||
this.__language = language;
|
||||
this.__language = language || undefined;
|
||||
this.__isSyntaxHighlightSupported = isLanguageSupportedByPrism(language);
|
||||
}
|
||||
|
||||
@ -212,11 +213,13 @@ 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;
|
||||
return $createCodeNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
updateFromJSON(serializedNode: LexicalUpdateJSON<SerializedCodeNode>): this {
|
||||
return super
|
||||
.updateFromJSON(serializedNode)
|
||||
.setLanguage(serializedNode.language);
|
||||
}
|
||||
|
||||
exportJSON(): SerializedCodeNode {
|
||||
@ -317,11 +320,12 @@ export class CodeNode extends ElementNode {
|
||||
return true;
|
||||
}
|
||||
|
||||
setLanguage(language: string): void {
|
||||
setLanguage(language: string | null | undefined): this {
|
||||
const writable = this.getWritable();
|
||||
writable.__language = language;
|
||||
writable.__language = language || undefined;
|
||||
writable.__isSyntaxHighlightSupported =
|
||||
isLanguageSupportedByPrism(language);
|
||||
return writable;
|
||||
}
|
||||
|
||||
getLanguage(): string | null | undefined {
|
||||
|
@ -6,12 +6,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import type {
|
||||
EditorConfig,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedTextNode,
|
||||
} from 'lexical';
|
||||
import type {EditorConfig, LexicalNode, SerializedTextNode} from 'lexical';
|
||||
|
||||
import {addClassNamesToElement} from '@lexical/utils';
|
||||
import {$applyNodeReplacement, TextNode} from 'lexical';
|
||||
@ -26,10 +21,6 @@ export class HashtagNode extends TextNode {
|
||||
return new HashtagNode(node.__text, node.__key);
|
||||
}
|
||||
|
||||
constructor(text: string, key?: NodeKey) {
|
||||
super(text, key);
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
const element = super.createDOM(config);
|
||||
addClassNamesToElement(element, config.theme.hashtag);
|
||||
@ -37,12 +28,7 @@ export class HashtagNode extends TextNode {
|
||||
}
|
||||
|
||||
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;
|
||||
return $createHashtagNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
canInsertTextBefore(): boolean {
|
||||
|
@ -13,6 +13,7 @@ import type {
|
||||
EditorConfig,
|
||||
LexicalCommand,
|
||||
LexicalNode,
|
||||
LexicalUpdateJSON,
|
||||
NodeKey,
|
||||
Point,
|
||||
RangeSelection,
|
||||
@ -87,7 +88,11 @@ export class LinkNode extends ElementNode {
|
||||
);
|
||||
}
|
||||
|
||||
constructor(url: string, attributes: LinkAttributes = {}, key?: NodeKey) {
|
||||
constructor(
|
||||
url: string = '',
|
||||
attributes: LinkAttributes = {},
|
||||
key?: NodeKey,
|
||||
) {
|
||||
super(key);
|
||||
const {target = null, rel = null, title = null} = attributes;
|
||||
this.__url = url;
|
||||
@ -162,18 +167,17 @@ export class LinkNode extends ElementNode {
|
||||
};
|
||||
}
|
||||
|
||||
static importJSON(
|
||||
serializedNode: SerializedLinkNode | SerializedAutoLinkNode,
|
||||
): LinkNode {
|
||||
const node = $createLinkNode(serializedNode.url, {
|
||||
rel: serializedNode.rel,
|
||||
target: serializedNode.target,
|
||||
title: serializedNode.title,
|
||||
});
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setIndent(serializedNode.indent);
|
||||
node.setDirection(serializedNode.direction);
|
||||
return node;
|
||||
static importJSON(serializedNode: SerializedLinkNode): LinkNode {
|
||||
return $createLinkNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
updateFromJSON(serializedNode: LexicalUpdateJSON<SerializedLinkNode>): this {
|
||||
return super
|
||||
.updateFromJSON(serializedNode)
|
||||
.setURL(serializedNode.url)
|
||||
.setRel(serializedNode.rel || null)
|
||||
.setTarget(serializedNode.target || null)
|
||||
.setTitle(serializedNode.title || null);
|
||||
}
|
||||
|
||||
sanitizeUrl(url: string): string {
|
||||
@ -203,36 +207,40 @@ export class LinkNode extends ElementNode {
|
||||
return this.getLatest().__url;
|
||||
}
|
||||
|
||||
setURL(url: string): void {
|
||||
setURL(url: string): this {
|
||||
const writable = this.getWritable();
|
||||
writable.__url = url;
|
||||
return writable;
|
||||
}
|
||||
|
||||
getTarget(): null | string {
|
||||
return this.getLatest().__target;
|
||||
}
|
||||
|
||||
setTarget(target: null | string): void {
|
||||
setTarget(target: null | string): this {
|
||||
const writable = this.getWritable();
|
||||
writable.__target = target;
|
||||
return writable;
|
||||
}
|
||||
|
||||
getRel(): null | string {
|
||||
return this.getLatest().__rel;
|
||||
}
|
||||
|
||||
setRel(rel: null | string): void {
|
||||
setRel(rel: null | string): this {
|
||||
const writable = this.getWritable();
|
||||
writable.__rel = rel;
|
||||
return writable;
|
||||
}
|
||||
|
||||
getTitle(): null | string {
|
||||
return this.getLatest().__title;
|
||||
}
|
||||
|
||||
setTitle(title: null | string): void {
|
||||
setTitle(title: null | string): this {
|
||||
const writable = this.getWritable();
|
||||
writable.__title = title;
|
||||
return writable;
|
||||
}
|
||||
|
||||
insertNewAfter(
|
||||
@ -316,7 +324,7 @@ function $convertAnchorElement(domNode: Node): DOMConversionOutput {
|
||||
* @returns The LinkNode.
|
||||
*/
|
||||
export function $createLinkNode(
|
||||
url: string,
|
||||
url: string = '',
|
||||
attributes?: LinkAttributes,
|
||||
): LinkNode {
|
||||
return $applyNodeReplacement(new LinkNode(url, attributes));
|
||||
@ -347,7 +355,11 @@ export class AutoLinkNode extends LinkNode {
|
||||
/** Indicates whether the autolink was ever unlinked. **/
|
||||
__isUnlinked: boolean;
|
||||
|
||||
constructor(url: string, attributes: AutoLinkAttributes = {}, key?: NodeKey) {
|
||||
constructor(
|
||||
url: string = '',
|
||||
attributes: AutoLinkAttributes = {},
|
||||
key?: NodeKey,
|
||||
) {
|
||||
super(url, attributes, key);
|
||||
this.__isUnlinked =
|
||||
attributes.isUnlinked !== undefined && attributes.isUnlinked !== null
|
||||
@ -376,7 +388,7 @@ export class AutoLinkNode extends LinkNode {
|
||||
return this.__isUnlinked;
|
||||
}
|
||||
|
||||
setIsUnlinked(value: boolean) {
|
||||
setIsUnlinked(value: boolean): this {
|
||||
const self = this.getWritable();
|
||||
self.__isUnlinked = value;
|
||||
return self;
|
||||
@ -402,16 +414,15 @@ export class AutoLinkNode extends LinkNode {
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedAutoLinkNode): AutoLinkNode {
|
||||
const node = $createAutoLinkNode(serializedNode.url, {
|
||||
isUnlinked: serializedNode.isUnlinked,
|
||||
rel: serializedNode.rel,
|
||||
target: serializedNode.target,
|
||||
title: serializedNode.title,
|
||||
});
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setIndent(serializedNode.indent);
|
||||
node.setDirection(serializedNode.direction);
|
||||
return node;
|
||||
return $createAutoLinkNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
updateFromJSON(
|
||||
serializedNode: LexicalUpdateJSON<SerializedAutoLinkNode>,
|
||||
): this {
|
||||
return super
|
||||
.updateFromJSON(serializedNode)
|
||||
.setIsUnlinked(serializedNode.isUnlinked || false);
|
||||
}
|
||||
|
||||
static importDOM(): null {
|
||||
@ -456,7 +467,7 @@ export class AutoLinkNode extends LinkNode {
|
||||
* @returns The LinkNode.
|
||||
*/
|
||||
export function $createAutoLinkNode(
|
||||
url: string,
|
||||
url: string = '',
|
||||
attributes?: AutoLinkAttributes,
|
||||
): AutoLinkNode {
|
||||
return $applyNodeReplacement(new AutoLinkNode(url, attributes));
|
||||
|
@ -15,6 +15,7 @@ import type {
|
||||
EditorConfig,
|
||||
EditorThemeClasses,
|
||||
LexicalNode,
|
||||
LexicalUpdateJSON,
|
||||
NodeKey,
|
||||
ParagraphNode,
|
||||
RangeSelection,
|
||||
@ -123,12 +124,16 @@ export class ListItemNode extends ElementNode {
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedListItemNode): ListItemNode {
|
||||
const node = $createListItemNode();
|
||||
node.setChecked(serializedNode.checked);
|
||||
node.setValue(serializedNode.value);
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setDirection(serializedNode.direction);
|
||||
return node;
|
||||
return $createListItemNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
updateFromJSON(
|
||||
serializedNode: LexicalUpdateJSON<SerializedListItemNode>,
|
||||
): this {
|
||||
return super
|
||||
.updateFromJSON(serializedNode)
|
||||
.setValue(serializedNode.value)
|
||||
.setChecked(serializedNode.checked);
|
||||
}
|
||||
|
||||
exportDOM(editor: LexicalEditor): DOMExportOutput {
|
||||
@ -257,9 +262,10 @@ export class ListItemNode extends ElementNode {
|
||||
_: RangeSelection,
|
||||
restoreSelection = true,
|
||||
): ListItemNode | ParagraphNode {
|
||||
const newElement = $createListItemNode(
|
||||
this.__checked == null ? undefined : false,
|
||||
);
|
||||
const newElement = $createListItemNode()
|
||||
.updateFromJSON(this.exportJSON())
|
||||
.setChecked(this.getChecked() ? false : undefined);
|
||||
|
||||
this.insertAfter(newElement, restoreSelection);
|
||||
|
||||
return newElement;
|
||||
@ -310,9 +316,10 @@ export class ListItemNode extends ElementNode {
|
||||
return self.__value;
|
||||
}
|
||||
|
||||
setValue(value: number): void {
|
||||
setValue(value: number): this {
|
||||
const self = this.getWritable();
|
||||
self.__value = value;
|
||||
return self;
|
||||
}
|
||||
|
||||
getChecked(): boolean | undefined {
|
||||
@ -328,13 +335,15 @@ export class ListItemNode extends ElementNode {
|
||||
return listType === 'check' ? Boolean(self.__checked) : undefined;
|
||||
}
|
||||
|
||||
setChecked(checked?: boolean): void {
|
||||
setChecked(checked?: boolean): this {
|
||||
const self = this.getWritable();
|
||||
self.__checked = checked;
|
||||
return self;
|
||||
}
|
||||
|
||||
toggleChecked(): void {
|
||||
this.setChecked(!this.__checked);
|
||||
toggleChecked(): this {
|
||||
const self = this.getWritable();
|
||||
return self.setChecked(!self.__checked);
|
||||
}
|
||||
|
||||
getIndent(): number {
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
ElementNode,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
LexicalUpdateJSON,
|
||||
NodeKey,
|
||||
SerializedElementNode,
|
||||
Spread,
|
||||
@ -69,7 +70,7 @@ export class ListNode extends ElementNode {
|
||||
return new ListNode(listType, node.__start, node.__key);
|
||||
}
|
||||
|
||||
constructor(listType: ListType, start: number, key?: NodeKey) {
|
||||
constructor(listType: ListType = 'number', start: number = 1, key?: NodeKey) {
|
||||
super(key);
|
||||
const _listType = TAG_TO_LIST_TYPE[listType] || listType;
|
||||
this.__listType = _listType;
|
||||
@ -81,10 +82,11 @@ export class ListNode extends ElementNode {
|
||||
return this.__tag;
|
||||
}
|
||||
|
||||
setListType(type: ListType): void {
|
||||
setListType(type: ListType): this {
|
||||
const writable = this.getWritable();
|
||||
writable.__listType = type;
|
||||
writable.__tag = type === 'number' ? 'ol' : 'ul';
|
||||
return writable;
|
||||
}
|
||||
|
||||
getListType(): ListType {
|
||||
@ -95,6 +97,12 @@ export class ListNode extends ElementNode {
|
||||
return this.__start;
|
||||
}
|
||||
|
||||
setStart(start: number): this {
|
||||
const self = this.getWritable();
|
||||
self.__start = start;
|
||||
return self;
|
||||
}
|
||||
|
||||
// View
|
||||
|
||||
createDOM(config: EditorConfig, _editor?: LexicalEditor): HTMLElement {
|
||||
@ -143,11 +151,14 @@ 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;
|
||||
return $createListNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
updateFromJSON(serializedNode: LexicalUpdateJSON<SerializedListNode>): this {
|
||||
return super
|
||||
.updateFromJSON(serializedNode)
|
||||
.setListType(serializedNode.listType)
|
||||
.setStart(serializedNode.start);
|
||||
}
|
||||
|
||||
exportDOM(editor: LexicalEditor): DOMExportOutput {
|
||||
@ -349,7 +360,10 @@ const TAG_TO_LIST_TYPE: Record<string, ListType> = {
|
||||
* @param start - Where an ordered list starts its count, start = 1 if left undefined.
|
||||
* @returns The new ListNode
|
||||
*/
|
||||
export function $createListNode(listType: ListType, start = 1): ListNode {
|
||||
export function $createListNode(
|
||||
listType: ListType = 'number',
|
||||
start = 1,
|
||||
): ListNode {
|
||||
return $applyNodeReplacement(new ListNode(listType, start));
|
||||
}
|
||||
|
||||
|
@ -57,9 +57,9 @@ describe('LexicalListNode tests', () => {
|
||||
expect(listNode.getTag()).toBe('ul');
|
||||
expect(listNode.getTextContent()).toBe('');
|
||||
});
|
||||
|
||||
// @ts-expect-error
|
||||
expect(() => $createListNode()).toThrow();
|
||||
await editor.update(() => {
|
||||
expect(() => $createListNode()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
test('ListNode.getTag()', async () => {
|
||||
|
@ -10,6 +10,7 @@ import type {
|
||||
BaseSelection,
|
||||
EditorConfig,
|
||||
LexicalNode,
|
||||
LexicalUpdateJSON,
|
||||
NodeKey,
|
||||
RangeSelection,
|
||||
SerializedElementNode,
|
||||
@ -29,6 +30,8 @@ export type SerializedMarkNode = Spread<
|
||||
SerializedElementNode
|
||||
>;
|
||||
|
||||
const NO_IDS: readonly string[] = [];
|
||||
|
||||
/** @noInheritDoc */
|
||||
export class MarkNode extends ElementNode {
|
||||
/** @internal */
|
||||
@ -47,23 +50,23 @@ export class MarkNode extends ElementNode {
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedMarkNode): MarkNode {
|
||||
const node = $createMarkNode(serializedNode.ids);
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setIndent(serializedNode.indent);
|
||||
node.setDirection(serializedNode.direction);
|
||||
return node;
|
||||
return $createMarkNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
updateFromJSON(serializedNode: LexicalUpdateJSON<SerializedMarkNode>): this {
|
||||
return super.updateFromJSON(serializedNode).setIDs(serializedNode.ids);
|
||||
}
|
||||
|
||||
exportJSON(): SerializedMarkNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
ids: Array.from(this.getIDs()),
|
||||
ids: this.getIDs(),
|
||||
};
|
||||
}
|
||||
|
||||
constructor(ids: readonly string[], key?: NodeKey) {
|
||||
constructor(ids: readonly string[] = NO_IDS, key?: NodeKey) {
|
||||
super(key);
|
||||
this.__ids = ids || [];
|
||||
this.__ids = ids;
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
@ -99,47 +102,33 @@ export class MarkNode extends ElementNode {
|
||||
}
|
||||
|
||||
hasID(id: string): boolean {
|
||||
const ids = this.getIDs();
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
if (id === ids[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return this.getIDs().includes(id);
|
||||
}
|
||||
|
||||
getIDs(): Array<string> {
|
||||
const self = this.getLatest();
|
||||
return $isMarkNode(self) ? Array.from(self.__ids) : [];
|
||||
return Array.from(this.getLatest().__ids);
|
||||
}
|
||||
|
||||
addID(id: string): void {
|
||||
setIDs(ids: readonly string[]): this {
|
||||
const self = this.getWritable();
|
||||
if ($isMarkNode(self)) {
|
||||
const ids = Array.from(self.__ids);
|
||||
self.__ids = ids;
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
// If we already have it, don't add again
|
||||
if (id === ids[i]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
ids.push(id);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
deleteID(id: string): void {
|
||||
addID(id: string): this {
|
||||
const self = this.getWritable();
|
||||
if ($isMarkNode(self)) {
|
||||
return self.__ids.includes(id) ? self.setIDs([...self.__ids, id]) : self;
|
||||
}
|
||||
|
||||
deleteID(id: string): this {
|
||||
const self = this.getWritable();
|
||||
const idx = self.__ids.indexOf(id);
|
||||
if (idx === -1) {
|
||||
return self;
|
||||
}
|
||||
const ids = Array.from(self.__ids);
|
||||
self.__ids = ids;
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
if (id === ids[i]) {
|
||||
ids.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
ids.splice(idx, 1);
|
||||
return self.setIDs(ids);
|
||||
}
|
||||
|
||||
insertNewAfter(
|
||||
@ -195,7 +184,7 @@ export class MarkNode extends ElementNode {
|
||||
}
|
||||
}
|
||||
|
||||
export function $createMarkNode(ids: readonly string[]): MarkNode {
|
||||
export function $createMarkNode(ids: readonly string[] = NO_IDS): MarkNode {
|
||||
return $applyNodeReplacement(new MarkNode(ids));
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,6 @@
|
||||
import type {
|
||||
EditorConfig,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
RangeSelection,
|
||||
SerializedElementNode,
|
||||
} from 'lexical';
|
||||
@ -30,18 +29,13 @@ export class OverflowNode extends ElementNode {
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedOverflowNode): OverflowNode {
|
||||
return $createOverflowNode();
|
||||
return $createOverflowNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
static importDOM(): null {
|
||||
return null;
|
||||
}
|
||||
|
||||
constructor(key?: NodeKey) {
|
||||
super(key);
|
||||
this.__type = 'overflow';
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
const div = document.createElement('span');
|
||||
const className = config.theme.characterLimit;
|
||||
|
@ -9,6 +9,7 @@
|
||||
import {expect} from '@playwright/test';
|
||||
|
||||
import {
|
||||
indent,
|
||||
moveLeft,
|
||||
moveRight,
|
||||
moveToEditorBeginning,
|
||||
@ -1901,4 +1902,48 @@ test.describe.parallel('Nested List', () => {
|
||||
});
|
||||
},
|
||||
);
|
||||
test('new list item should preserve format from previous list item even after new list item is indented', async ({
|
||||
page,
|
||||
}) => {
|
||||
await focusEditor(page);
|
||||
await toggleBulletList(page);
|
||||
await toggleBold(page);
|
||||
await page.keyboard.type('MLH Fellowship');
|
||||
await page.keyboard.press('Enter');
|
||||
await indent(page, 1);
|
||||
await page.keyboard.type('Fall 2024');
|
||||
await assertHTML(
|
||||
page,
|
||||
html`
|
||||
<ul class="PlaygroundEditorTheme__ul">
|
||||
<li
|
||||
class="PlaygroundEditorTheme__listItem PlaygroundEditorTheme__ltr"
|
||||
dir="ltr"
|
||||
value="1">
|
||||
<strong
|
||||
class="PlaygroundEditorTheme__textBold"
|
||||
data-lexical-text="true">
|
||||
MLH Fellowship
|
||||
</strong>
|
||||
</li>
|
||||
<li
|
||||
class="PlaygroundEditorTheme__listItem PlaygroundEditorTheme__nestedListItem"
|
||||
value="2">
|
||||
<ul class="PlaygroundEditorTheme__ul">
|
||||
<li
|
||||
class="PlaygroundEditorTheme__listItem PlaygroundEditorTheme__ltr"
|
||||
dir="ltr"
|
||||
value="1">
|
||||
<strong
|
||||
class="PlaygroundEditorTheme__textBold"
|
||||
data-lexical-text="true">
|
||||
Fall 2024
|
||||
</strong>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -48,15 +48,10 @@ export class AutocompleteNode extends TextNode {
|
||||
static importJSON(
|
||||
serializedNode: SerializedAutocompleteNode,
|
||||
): AutocompleteNode {
|
||||
const node = $createAutocompleteNode(
|
||||
return $createAutocompleteNode(
|
||||
serializedNode.text,
|
||||
serializedNode.uuid,
|
||||
);
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setDetail(serializedNode.detail);
|
||||
node.setMode(serializedNode.mode);
|
||||
node.setStyle(serializedNode.style);
|
||||
return node;
|
||||
).updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
exportJSON(): SerializedAutocompleteNode {
|
||||
|
@ -58,15 +58,10 @@ export class EmojiNode extends TextNode {
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedEmojiNode): EmojiNode {
|
||||
const node = $createEmojiNode(
|
||||
return $createEmojiNode(
|
||||
serializedNode.className,
|
||||
serializedNode.text,
|
||||
);
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setDetail(serializedNode.detail);
|
||||
node.setMode(serializedNode.mode);
|
||||
node.setStyle(serializedNode.style);
|
||||
return node;
|
||||
).updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
exportJSON(): SerializedEmojiNode {
|
||||
|
@ -65,11 +65,10 @@ export class EquationNode extends DecoratorNode<JSX.Element> {
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedEquationNode): EquationNode {
|
||||
const node = $createEquationNode(
|
||||
return $createEquationNode(
|
||||
serializedNode.equation,
|
||||
serializedNode.inline,
|
||||
);
|
||||
return node;
|
||||
).updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
exportJSON(): SerializedEquationNode {
|
||||
|
@ -79,7 +79,7 @@ export class ExcalidrawNode extends DecoratorNode<JSX.Element> {
|
||||
serializedNode.data,
|
||||
serializedNode.width ?? 'inherit',
|
||||
serializedNode.height ?? 'inherit',
|
||||
);
|
||||
).updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
exportJSON(): SerializedExcalidrawNode {
|
||||
|
@ -73,9 +73,9 @@ export class FigmaNode extends DecoratorBlockNode {
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedFigmaNode): FigmaNode {
|
||||
const node = $createFigmaNode(serializedNode.documentID);
|
||||
node.setFormat(serializedNode.format);
|
||||
return node;
|
||||
return $createFigmaNode(serializedNode.documentID).updateFromJSON(
|
||||
serializedNode,
|
||||
);
|
||||
}
|
||||
|
||||
exportJSON(): SerializedFigmaNode {
|
||||
|
@ -13,6 +13,7 @@ import type {
|
||||
EditorConfig,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
LexicalUpdateJSON,
|
||||
NodeKey,
|
||||
SerializedEditor,
|
||||
SerializedLexicalNode,
|
||||
@ -99,16 +100,21 @@ export class ImageNode extends DecoratorNode<JSX.Element> {
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedImageNode): ImageNode {
|
||||
const {altText, height, width, maxWidth, caption, src, showCaption} =
|
||||
serializedNode;
|
||||
const node = $createImageNode({
|
||||
const {altText, height, width, maxWidth, src, showCaption} = serializedNode;
|
||||
return $createImageNode({
|
||||
altText,
|
||||
height,
|
||||
maxWidth,
|
||||
showCaption,
|
||||
src,
|
||||
width,
|
||||
});
|
||||
}).updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
updateFromJSON(serializedNode: LexicalUpdateJSON<SerializedImageNode>): this {
|
||||
const node = super.updateFromJSON(serializedNode);
|
||||
const {caption} = serializedNode;
|
||||
|
||||
const nestedEditor = node.__caption;
|
||||
const editorState = nestedEditor.parseEditorState(caption.editorState);
|
||||
if (!editorState.isEmpty()) {
|
||||
|
@ -13,6 +13,7 @@ import type {
|
||||
EditorConfig,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
LexicalUpdateJSON,
|
||||
NodeKey,
|
||||
SerializedEditor,
|
||||
SerializedLexicalNode,
|
||||
@ -100,16 +101,22 @@ export class InlineImageNode extends DecoratorNode<JSX.Element> {
|
||||
static importJSON(
|
||||
serializedNode: SerializedInlineImageNode,
|
||||
): InlineImageNode {
|
||||
const {altText, height, width, caption, src, showCaption, position} =
|
||||
serializedNode;
|
||||
const node = $createInlineImageNode({
|
||||
const {altText, height, width, src, showCaption, position} = serializedNode;
|
||||
return $createInlineImageNode({
|
||||
altText,
|
||||
height,
|
||||
position,
|
||||
showCaption,
|
||||
src,
|
||||
width,
|
||||
});
|
||||
}).updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
updateFromJSON(
|
||||
serializedNode: LexicalUpdateJSON<SerializedInlineImageNode>,
|
||||
): this {
|
||||
const {caption} = serializedNode;
|
||||
const node = super.updateFromJSON(serializedNode);
|
||||
const nestedEditor = node.__caption;
|
||||
const editorState = nestedEditor.parseEditorState(caption.editorState);
|
||||
if (!editorState.isEmpty()) {
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
import type {EditorConfig, LexicalNode, SerializedTextNode} from 'lexical';
|
||||
|
||||
import {TextNode} from 'lexical';
|
||||
import {$applyNodeReplacement, TextNode} from 'lexical';
|
||||
|
||||
export type SerializedKeywordNode = SerializedTextNode;
|
||||
|
||||
@ -22,12 +22,7 @@ export class KeywordNode extends TextNode {
|
||||
}
|
||||
|
||||
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;
|
||||
return $createKeywordNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
@ -50,8 +45,8 @@ export class KeywordNode extends TextNode {
|
||||
}
|
||||
}
|
||||
|
||||
export function $createKeywordNode(keyword: string): KeywordNode {
|
||||
return new KeywordNode(keyword);
|
||||
export function $createKeywordNode(keyword: string = ''): KeywordNode {
|
||||
return $applyNodeReplacement(new KeywordNode(keyword));
|
||||
}
|
||||
|
||||
export function $isKeywordNode(node: LexicalNode | null | undefined): boolean {
|
||||
|
@ -12,6 +12,7 @@ import type {
|
||||
DOMExportOutput,
|
||||
EditorConfig,
|
||||
LexicalNode,
|
||||
LexicalUpdateJSON,
|
||||
NodeKey,
|
||||
SerializedElementNode,
|
||||
Spread,
|
||||
@ -95,7 +96,15 @@ export class LayoutContainerNode extends ElementNode {
|
||||
}
|
||||
|
||||
static importJSON(json: SerializedLayoutContainerNode): LayoutContainerNode {
|
||||
return $createLayoutContainerNode(json.templateColumns);
|
||||
return $createLayoutContainerNode().updateFromJSON(json);
|
||||
}
|
||||
|
||||
updateFromJSON(
|
||||
serializedNode: LexicalUpdateJSON<SerializedLayoutContainerNode>,
|
||||
): this {
|
||||
return super
|
||||
.updateFromJSON(serializedNode)
|
||||
.setTemplateColumns(serializedNode.templateColumns);
|
||||
}
|
||||
|
||||
isShadowRoot(): boolean {
|
||||
@ -117,13 +126,15 @@ export class LayoutContainerNode extends ElementNode {
|
||||
return this.getLatest().__templateColumns;
|
||||
}
|
||||
|
||||
setTemplateColumns(templateColumns: string) {
|
||||
this.getWritable().__templateColumns = templateColumns;
|
||||
setTemplateColumns(templateColumns: string): this {
|
||||
const self = this.getWritable();
|
||||
self.__templateColumns = templateColumns;
|
||||
return self;
|
||||
}
|
||||
}
|
||||
|
||||
export function $createLayoutContainerNode(
|
||||
templateColumns: string,
|
||||
templateColumns: string = '',
|
||||
): LayoutContainerNode {
|
||||
return new LayoutContainerNode(templateColumns);
|
||||
}
|
||||
|
@ -59,8 +59,8 @@ export class LayoutItemNode extends ElementNode {
|
||||
};
|
||||
}
|
||||
|
||||
static importJSON(): LayoutItemNode {
|
||||
return $createLayoutItemNode();
|
||||
static importJSON(serializedNode: SerializedLayoutItemNode): LayoutItemNode {
|
||||
return $createLayoutItemNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
isShadowRoot(): boolean {
|
||||
|
@ -30,9 +30,13 @@ function $convertMentionElement(
|
||||
domNode: HTMLElement,
|
||||
): DOMConversionOutput | null {
|
||||
const textContent = domNode.textContent;
|
||||
const mentionName = domNode.getAttribute('data-lexical-mention-name');
|
||||
|
||||
if (textContent !== null) {
|
||||
const node = $createMentionNode(textContent);
|
||||
const node = $createMentionNode(
|
||||
typeof mentionName === 'string' ? mentionName : textContent,
|
||||
textContent,
|
||||
);
|
||||
return {
|
||||
node,
|
||||
};
|
||||
@ -53,13 +57,9 @@ export class MentionNode extends TextNode {
|
||||
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;
|
||||
return $createMentionNode(serializedNode.mentionName).updateFromJSON(
|
||||
serializedNode,
|
||||
);
|
||||
}
|
||||
|
||||
constructor(mentionName: string, text?: string, key?: NodeKey) {
|
||||
@ -85,6 +85,9 @@ export class MentionNode extends TextNode {
|
||||
exportDOM(): DOMExportOutput {
|
||||
const element = document.createElement('span');
|
||||
element.setAttribute('data-lexical-mention', 'true');
|
||||
if (this.__text !== this.__mention) {
|
||||
element.setAttribute('data-lexical-mention-name', this.__mention);
|
||||
}
|
||||
element.textContent = this.__text;
|
||||
return {element};
|
||||
}
|
||||
@ -116,8 +119,11 @@ export class MentionNode extends TextNode {
|
||||
}
|
||||
}
|
||||
|
||||
export function $createMentionNode(mentionName: string): MentionNode {
|
||||
const mentionNode = new MentionNode(mentionName);
|
||||
export function $createMentionNode(
|
||||
mentionName: string,
|
||||
textContent?: string,
|
||||
): MentionNode {
|
||||
const mentionNode = new MentionNode(mentionName, (textContent = mentionName));
|
||||
mentionNode.setMode('segmented').toggleDirectionless();
|
||||
return $applyNodeReplacement(mentionNode);
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ export class PageBreakNode extends DecoratorNode<JSX.Element> {
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedPageBreakNode): PageBreakNode {
|
||||
return $createPageBreakNode();
|
||||
return $createPageBreakNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
static importDOM(): DOMConversionMap | null {
|
||||
|
@ -87,12 +87,10 @@ export class PollNode extends DecoratorNode<JSX.Element> {
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedPollNode): PollNode {
|
||||
const node = $createPollNode(
|
||||
return $createPollNode(
|
||||
serializedNode.question,
|
||||
serializedNode.options,
|
||||
);
|
||||
serializedNode.options.forEach(node.addOption);
|
||||
return node;
|
||||
).updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
constructor(question: string, options: Options, key?: NodeKey) {
|
||||
|
@ -6,12 +6,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import type {
|
||||
EditorConfig,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedTextNode,
|
||||
} from 'lexical';
|
||||
import type {EditorConfig, LexicalNode, SerializedTextNode} from 'lexical';
|
||||
|
||||
import {addClassNamesToElement} from '@lexical/utils';
|
||||
import {$applyNodeReplacement, TextNode} from 'lexical';
|
||||
@ -26,10 +21,6 @@ export class SpecialTextNode extends TextNode {
|
||||
return new SpecialTextNode(node.__text, node.__key);
|
||||
}
|
||||
|
||||
constructor(text: string, key?: NodeKey) {
|
||||
super(text, key);
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
const dom = document.createElement('span');
|
||||
addClassNamesToElement(dom, config.theme.specialText);
|
||||
@ -49,12 +40,7 @@ export class SpecialTextNode extends TextNode {
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedTextNode): SpecialTextNode {
|
||||
const node = $createSpecialTextNode(serializedNode.text);
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setStyle(serializedNode.style);
|
||||
node.setDetail(serializedNode.detail);
|
||||
node.setMode(serializedNode.mode);
|
||||
return node;
|
||||
return $createSpecialTextNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
isTextEntity(): true {
|
||||
|
@ -10,6 +10,7 @@ import type {
|
||||
EditorConfig,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
LexicalUpdateJSON,
|
||||
NodeKey,
|
||||
SerializedEditor,
|
||||
SerializedLexicalNode,
|
||||
@ -55,11 +56,17 @@ export class StickyNode extends DecoratorNode<JSX.Element> {
|
||||
);
|
||||
}
|
||||
static importJSON(serializedNode: SerializedStickyNode): StickyNode {
|
||||
const stickyNode = new StickyNode(
|
||||
return new StickyNode(
|
||||
serializedNode.xOffset,
|
||||
serializedNode.yOffset,
|
||||
serializedNode.color,
|
||||
);
|
||||
).updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
updateFromJSON(
|
||||
serializedNode: LexicalUpdateJSON<SerializedStickyNode>,
|
||||
): this {
|
||||
const stickyNode = super.updateFromJSON(serializedNode);
|
||||
const caption = serializedNode.caption;
|
||||
const nestedEditor = stickyNode.__caption;
|
||||
const editorState = nestedEditor.parseEditorState(caption.editorState);
|
||||
|
@ -142,9 +142,7 @@ export class TweetNode extends DecoratorBlockNode {
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedTweetNode): TweetNode {
|
||||
const node = $createTweetNode(serializedNode.id);
|
||||
node.setFormat(serializedNode.format);
|
||||
return node;
|
||||
return $createTweetNode(serializedNode.id).updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
exportJSON(): SerializedTweetNode {
|
||||
|
@ -89,9 +89,9 @@ export class YouTubeNode extends DecoratorBlockNode {
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedYouTubeNode): YouTubeNode {
|
||||
const node = $createYouTubeNode(serializedNode.videoID);
|
||||
node.setFormat(serializedNode.format);
|
||||
return node;
|
||||
return $createYouTubeNode(serializedNode.videoID).updateFromJSON(
|
||||
serializedNode,
|
||||
);
|
||||
}
|
||||
|
||||
exportJSON(): SerializedYouTubeNode {
|
||||
|
@ -118,8 +118,9 @@ export class CollapsibleContainerNode extends ElementNode {
|
||||
static importJSON(
|
||||
serializedNode: SerializedCollapsibleContainerNode,
|
||||
): CollapsibleContainerNode {
|
||||
const node = $createCollapsibleContainerNode(serializedNode.open);
|
||||
return node;
|
||||
return $createCollapsibleContainerNode(serializedNode.open).updateFromJSON(
|
||||
serializedNode,
|
||||
);
|
||||
}
|
||||
|
||||
exportDOM(): DOMExportOutput {
|
||||
|
@ -100,7 +100,7 @@ export class CollapsibleContentNode extends ElementNode {
|
||||
static importJSON(
|
||||
serializedNode: SerializedCollapsibleContentNode,
|
||||
): CollapsibleContentNode {
|
||||
return $createCollapsibleContentNode();
|
||||
return $createCollapsibleContentNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
isShadowRoot(): boolean {
|
||||
|
@ -80,7 +80,7 @@ export class CollapsibleTitleNode extends ElementNode {
|
||||
static importJSON(
|
||||
serializedNode: SerializedCollapsibleTitleNode,
|
||||
): CollapsibleTitleNode {
|
||||
return $createCollapsibleTitleNode();
|
||||
return $createCollapsibleTitleNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
collapseAtStart(_selection: RangeSelection): boolean {
|
||||
|
@ -22,7 +22,7 @@ import {
|
||||
DRAGOVER_COMMAND,
|
||||
DRAGSTART_COMMAND,
|
||||
DROP_COMMAND,
|
||||
getDOMSelection,
|
||||
getDOMSelectionFromTarget,
|
||||
isHTMLElement,
|
||||
LexicalCommand,
|
||||
LexicalEditor,
|
||||
@ -367,14 +367,7 @@ function canDropImage(event: DragEvent): boolean {
|
||||
|
||||
function getDragSelection(event: DragEvent): Range | null | undefined {
|
||||
let range;
|
||||
const target = event.target as null | Element | Document;
|
||||
const targetWindow =
|
||||
target == null
|
||||
? null
|
||||
: target.nodeType === 9
|
||||
? (target as Document).defaultView
|
||||
: (target as Element).ownerDocument.defaultView;
|
||||
const domSelection = getDOMSelection(targetWindow);
|
||||
const domSelection = getDOMSelectionFromTarget(event.target);
|
||||
if (document.caretRangeFromPoint) {
|
||||
range = document.caretRangeFromPoint(event.clientX, event.clientY);
|
||||
} else if (event.rangeParent && domSelection !== null) {
|
||||
|
@ -26,7 +26,7 @@ import {
|
||||
DRAGOVER_COMMAND,
|
||||
DRAGSTART_COMMAND,
|
||||
DROP_COMMAND,
|
||||
getDOMSelection,
|
||||
getDOMSelectionFromTarget,
|
||||
isHTMLElement,
|
||||
LexicalCommand,
|
||||
LexicalEditor,
|
||||
@ -319,14 +319,7 @@ function canDropImage(event: DragEvent): boolean {
|
||||
|
||||
function getDragSelection(event: DragEvent): Range | null | undefined {
|
||||
let range;
|
||||
const target = event.target as null | Element | Document;
|
||||
const targetWindow =
|
||||
target == null
|
||||
? null
|
||||
: target.nodeType === 9
|
||||
? (target as Document).defaultView
|
||||
: (target as Element).ownerDocument.defaultView;
|
||||
const domSelection = getDOMSelection(targetWindow);
|
||||
const domSelection = getDOMSelectionFromTarget(event.target);
|
||||
if (document.caretRangeFromPoint) {
|
||||
range = document.caretRangeFromPoint(event.clientX, event.clientY);
|
||||
} else if (event.rangeParent && domSelection !== null) {
|
||||
|
@ -9,6 +9,7 @@
|
||||
import type {
|
||||
ElementFormatType,
|
||||
LexicalNode,
|
||||
LexicalUpdateJSON,
|
||||
NodeKey,
|
||||
SerializedLexicalNode,
|
||||
Spread,
|
||||
@ -38,6 +39,14 @@ export class DecoratorBlockNode extends DecoratorNode<JSX.Element> {
|
||||
};
|
||||
}
|
||||
|
||||
updateFromJSON(
|
||||
serializedNode: LexicalUpdateJSON<SerializedDecoratorBlockNode>,
|
||||
): this {
|
||||
return super
|
||||
.updateFromJSON(serializedNode)
|
||||
.setFormat(serializedNode.format || '');
|
||||
}
|
||||
|
||||
canIndent(): false {
|
||||
return false;
|
||||
}
|
||||
@ -50,9 +59,10 @@ export class DecoratorBlockNode extends DecoratorNode<JSX.Element> {
|
||||
return false;
|
||||
}
|
||||
|
||||
setFormat(format: ElementFormatType): void {
|
||||
setFormat(format: ElementFormatType): this {
|
||||
const self = this.getWritable();
|
||||
self.__format = format;
|
||||
return self;
|
||||
}
|
||||
|
||||
isInline(): false {
|
||||
|
@ -126,7 +126,7 @@ export class HorizontalRuleNode extends DecoratorNode<JSX.Element> {
|
||||
static importJSON(
|
||||
serializedNode: SerializedHorizontalRuleNode,
|
||||
): HorizontalRuleNode {
|
||||
return $createHorizontalRuleNode();
|
||||
return $createHorizontalRuleNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
static importDOM(): DOMConversionMap | null {
|
||||
|
@ -74,13 +74,26 @@ describe('LexicalHeadingNode tests', () => {
|
||||
test('HeadingNode.updateDOM()', async () => {
|
||||
const {editor} = testEnv;
|
||||
await editor.update(() => {
|
||||
const headingNode = new HeadingNode('h1');
|
||||
const headingNode = $createHeadingNode('h1');
|
||||
const domElement = headingNode.createDOM(editorConfig);
|
||||
expect(domElement.outerHTML).toBe('<h1 class="my-h1-class"></h1>');
|
||||
const newHeadingNode = new HeadingNode('h2');
|
||||
const result = newHeadingNode.updateDOM(headingNode, domElement);
|
||||
const newHeadingNode = $createHeadingNode('h1');
|
||||
const result = newHeadingNode.updateDOM(
|
||||
headingNode,
|
||||
domElement,
|
||||
editor._config,
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
expect(domElement.outerHTML).toBe('<h1 class="my-h1-class"></h1>');
|
||||
// When the HTML tag changes we must return true and not update the DOM, as createDOM will be called
|
||||
const newTag = $createHeadingNode('h2');
|
||||
const newTagResult = newTag.updateDOM(
|
||||
headingNode,
|
||||
domElement,
|
||||
editor._config,
|
||||
);
|
||||
expect(newTagResult).toBe(true);
|
||||
expect(domElement.outerHTML).toBe('<h1 class="my-h1-class"></h1>');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -16,6 +16,7 @@ import type {
|
||||
LexicalCommand,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
LexicalUpdateJSON,
|
||||
NodeKey,
|
||||
ParagraphNode,
|
||||
PasteCommandType,
|
||||
@ -128,10 +129,6 @@ export class QuoteNode extends ElementNode {
|
||||
return new QuoteNode(node.__key);
|
||||
}
|
||||
|
||||
constructor(key?: NodeKey) {
|
||||
super(key);
|
||||
}
|
||||
|
||||
// View
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
@ -175,11 +172,7 @@ export class QuoteNode extends ElementNode {
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedQuoteNode): QuoteNode {
|
||||
const node = $createQuoteNode();
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setIndent(serializedNode.indent);
|
||||
node.setDirection(serializedNode.direction);
|
||||
return node;
|
||||
return $createQuoteNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
// Mutation
|
||||
@ -239,6 +232,12 @@ export class HeadingNode extends ElementNode {
|
||||
return this.__tag;
|
||||
}
|
||||
|
||||
setTag(tag: HeadingTagType): this {
|
||||
const self = this.getWritable();
|
||||
this.__tag = tag;
|
||||
return self;
|
||||
}
|
||||
|
||||
// View
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
@ -253,8 +252,8 @@ export class HeadingNode extends ElementNode {
|
||||
return element;
|
||||
}
|
||||
|
||||
updateDOM(prevNode: this, dom: HTMLElement): boolean {
|
||||
return false;
|
||||
updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean {
|
||||
return prevNode.__tag !== this.__tag;
|
||||
}
|
||||
|
||||
static importDOM(): DOMConversionMap | null {
|
||||
@ -334,11 +333,15 @@ 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;
|
||||
return $createHeadingNode(serializedNode.tag).updateFromJSON(
|
||||
serializedNode,
|
||||
);
|
||||
}
|
||||
|
||||
updateFromJSON(
|
||||
serializedNode: LexicalUpdateJSON<SerializedHeadingNode>,
|
||||
): this {
|
||||
return super.updateFromJSON(serializedNode).setTag(serializedNode.tag);
|
||||
}
|
||||
|
||||
exportJSON(): SerializedHeadingNode {
|
||||
@ -426,7 +429,9 @@ function $convertBlockquoteElement(element: HTMLElement): DOMConversionOutput {
|
||||
return {node};
|
||||
}
|
||||
|
||||
export function $createHeadingNode(headingTag: HeadingTagType): HeadingNode {
|
||||
export function $createHeadingNode(
|
||||
headingTag: HeadingTagType = 'h1',
|
||||
): HeadingNode {
|
||||
return $applyNodeReplacement(new HeadingNode(headingTag));
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ import type {
|
||||
EditorConfig,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
LexicalUpdateJSON,
|
||||
NodeKey,
|
||||
SerializedElementNode,
|
||||
Spread,
|
||||
@ -61,7 +62,7 @@ export class TableCellNode extends ElementNode {
|
||||
/** @internal */
|
||||
__headerState: TableCellHeaderState;
|
||||
/** @internal */
|
||||
__width?: number;
|
||||
__width?: number | undefined;
|
||||
/** @internal */
|
||||
__backgroundColor: null | string;
|
||||
|
||||
@ -98,14 +99,18 @@ export class TableCellNode extends ElementNode {
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedTableCellNode): TableCellNode {
|
||||
const colSpan = serializedNode.colSpan || 1;
|
||||
const rowSpan = serializedNode.rowSpan || 1;
|
||||
return $createTableCellNode(
|
||||
serializedNode.headerState,
|
||||
colSpan,
|
||||
serializedNode.width || undefined,
|
||||
)
|
||||
.setRowSpan(rowSpan)
|
||||
return $createTableCellNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
updateFromJSON(
|
||||
serializedNode: LexicalUpdateJSON<SerializedTableCellNode>,
|
||||
): this {
|
||||
return super
|
||||
.updateFromJSON(serializedNode)
|
||||
.setHeaderStyles(serializedNode.headerState)
|
||||
.setColSpan(serializedNode.colSpan || 1)
|
||||
.setRowSpan(serializedNode.rowSpan || 1)
|
||||
.setWidth(serializedNode.width || undefined)
|
||||
.setBackgroundColor(serializedNode.backgroundColor || null);
|
||||
}
|
||||
|
||||
@ -224,7 +229,7 @@ export class TableCellNode extends ElementNode {
|
||||
return this.getLatest().__headerState;
|
||||
}
|
||||
|
||||
setWidth(width: number): this {
|
||||
setWidth(width: number | undefined): this {
|
||||
const self = this.getWritable();
|
||||
self.__width = width;
|
||||
return self;
|
||||
|
@ -25,6 +25,7 @@ import {
|
||||
ElementNode,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
LexicalUpdateJSON,
|
||||
NodeKey,
|
||||
SerializedElementNode,
|
||||
setDOMUnmanaged,
|
||||
@ -114,21 +115,22 @@ export function setScrollableTablesActive(
|
||||
export class TableNode extends ElementNode {
|
||||
/** @internal */
|
||||
__rowStriping: boolean;
|
||||
__colWidths?: number[] | readonly number[];
|
||||
__colWidths?: readonly number[];
|
||||
|
||||
static getType(): string {
|
||||
return 'table';
|
||||
}
|
||||
|
||||
getColWidths(): number[] | readonly number[] | undefined {
|
||||
getColWidths(): readonly number[] | undefined {
|
||||
const self = this.getLatest();
|
||||
return self.__colWidths;
|
||||
}
|
||||
|
||||
setColWidths(colWidths: readonly number[]): this {
|
||||
setColWidths(colWidths: readonly number[] | undefined): this {
|
||||
const self = this.getWritable();
|
||||
// NOTE: Node properties should be immutable. Freeze to prevent accidental mutation.
|
||||
self.__colWidths = __DEV__ ? Object.freeze(colWidths) : colWidths;
|
||||
self.__colWidths =
|
||||
colWidths !== undefined && __DEV__ ? Object.freeze(colWidths) : colWidths;
|
||||
return self;
|
||||
}
|
||||
|
||||
@ -152,10 +154,14 @@ export class TableNode extends ElementNode {
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedTableNode): TableNode {
|
||||
const tableNode = $createTableNode();
|
||||
tableNode.__rowStriping = serializedNode.rowStriping || false;
|
||||
tableNode.__colWidths = serializedNode.colWidths;
|
||||
return tableNode;
|
||||
return $createTableNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
updateFromJSON(serializedNode: LexicalUpdateJSON<SerializedTableNode>): this {
|
||||
return super
|
||||
.updateFromJSON(serializedNode)
|
||||
.setRowStriping(serializedNode.rowStriping || false)
|
||||
.setColWidths(serializedNode.colWidths);
|
||||
}
|
||||
|
||||
constructor(key?: NodeKey) {
|
||||
@ -425,8 +431,10 @@ export class TableNode extends ElementNode {
|
||||
return Boolean(this.getLatest().__rowStriping);
|
||||
}
|
||||
|
||||
setRowStriping(newRowStriping: boolean): void {
|
||||
this.getWritable().__rowStriping = newRowStriping;
|
||||
setRowStriping(newRowStriping: boolean): this {
|
||||
const self = this.getWritable();
|
||||
self.__rowStriping = newRowStriping;
|
||||
return self;
|
||||
}
|
||||
|
||||
canSelectBefore(): true {
|
||||
|
@ -6,7 +6,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import type {BaseSelection, Spread} from 'lexical';
|
||||
import type {BaseSelection, LexicalUpdateJSON, Spread} from 'lexical';
|
||||
|
||||
import {$descendantsMatching, addClassNamesToElement} from '@lexical/utils';
|
||||
import {
|
||||
@ -53,7 +53,15 @@ export class TableRowNode extends ElementNode {
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedTableRowNode): TableRowNode {
|
||||
return $createTableRowNode(serializedNode.height);
|
||||
return $createTableRowNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
updateFromJSON(
|
||||
serializedNode: LexicalUpdateJSON<SerializedTableRowNode>,
|
||||
): this {
|
||||
return super
|
||||
.updateFromJSON(serializedNode)
|
||||
.setHeight(serializedNode.height);
|
||||
}
|
||||
|
||||
constructor(height?: number, key?: NodeKey) {
|
||||
@ -62,9 +70,10 @@ export class TableRowNode extends ElementNode {
|
||||
}
|
||||
|
||||
exportJSON(): SerializedTableRowNode {
|
||||
const height = this.getHeight();
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
...(this.getHeight() && {height: this.getHeight()}),
|
||||
...(height === undefined ? undefined : {height}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -92,10 +101,10 @@ export class TableRowNode extends ElementNode {
|
||||
return true;
|
||||
}
|
||||
|
||||
setHeight(height: number): number | null | undefined {
|
||||
setHeight(height?: number | undefined): this {
|
||||
const self = this.getWritable();
|
||||
self.__height = height;
|
||||
return this.__height;
|
||||
return self;
|
||||
}
|
||||
|
||||
getHeight(): number | undefined {
|
||||
|
@ -135,6 +135,8 @@ describe('table selection', () => {
|
||||
__prev: null,
|
||||
__size: 1,
|
||||
__style: '',
|
||||
__textFormat: 0,
|
||||
__textStyle: '',
|
||||
__type: 'root',
|
||||
});
|
||||
expect(parsedParagraph).toEqual({
|
||||
|
@ -203,7 +203,7 @@ exportJSON(): SerializedHeadingNode {
|
||||
|
||||
#### `LexicalNode.importJSON()`
|
||||
|
||||
You can control how a `LexicalNode` is serialized back into a node from JSON by adding an `importJSON()` method.
|
||||
You can control how a `LexicalNode` is deserialized back into a node from JSON by adding an `importJSON()` method.
|
||||
|
||||
```js
|
||||
export type SerializedLexicalNode = {
|
||||
@ -216,23 +216,62 @@ importJSON(jsonNode: SerializedLexicalNode): LexicalNode
|
||||
|
||||
This method works in the opposite way to how `exportJSON` works. Lexical uses the `type` field on the JSON object to determine what Lexical node class it needs to map to, so keeping the `type` field consistent with the `getType()` of the LexicalNode is essential.
|
||||
|
||||
You should use the `updateFromJSON` method in your `importJSON` to simplify the implementation and allow for future extension by the base classes.
|
||||
|
||||
Here's an example of `importJSON` for the `HeadingNode`:
|
||||
|
||||
```js
|
||||
```ts
|
||||
static importJSON(serializedNode: SerializedHeadingNode): HeadingNode {
|
||||
const node = $createHeadingNode(serializedNode.tag);
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setIndent(serializedNode.indent);
|
||||
node.setDirection(serializedNode.direction);
|
||||
return node;
|
||||
return $createHeadingNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
updateFromJSON(
|
||||
serializedNode: LexicalUpdateJSON<SerializedHeadingNode>,
|
||||
): this {
|
||||
return super.updateFromJSON(serializedNode).setTag(serializedNode.tag);
|
||||
}
|
||||
```
|
||||
|
||||
#### `LexicalNode.updateFromJSON()`
|
||||
|
||||
`updateFromJSON` is a method introduced in Lexical 0.23 to simplify the implementation of `importJSON`, so that a base class can expose the code that it is using to set all of the node's properties based on the JSON to any subclass.
|
||||
|
||||
:::note
|
||||
|
||||
The input type used in this method is not sound in the general case, but it is safe if subclasses only add optional properties to the JSON. Even though it is not sound, the usage in this library is safe as long as your `importJSON` method does not upcast the node before calling `updateFromJSON`.
|
||||
|
||||
```ts
|
||||
export type SerializedExtendedTextNode = Spread<
|
||||
// UNSAFE. This property is not optional
|
||||
{ newProperty: string },
|
||||
SerializedTextNode
|
||||
>;
|
||||
```
|
||||
|
||||
```ts
|
||||
export type SerializedExtendedTextNode = Spread<
|
||||
// SAFE. This property is not optional
|
||||
{ newProperty?: string },
|
||||
SerializedTextNode
|
||||
>;
|
||||
```
|
||||
|
||||
This is because it's possible to cast to a more general type, e.g.
|
||||
|
||||
```ts
|
||||
const serializedNode: SerializedTextNode = { /* ... */ };
|
||||
const newNode: TextNode = $createExtendedTextNode();
|
||||
// This passes the type check, but would fail at runtime if the updateFromJSON method required newProperty
|
||||
newNode.updateFromJSON(serializedNode);
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Versioning & Breaking Changes
|
||||
|
||||
It's important to note that you should avoid making breaking changes to existing fields in your JSON object, especially if backwards compatibility is an important part of your editor. That's why we recommend using a version field to separate the different changes in your node as you add or change functionality of custom nodes. Here's the serialized type definition for Lexical's base `TextNode` class:
|
||||
|
||||
```js
|
||||
```ts
|
||||
import type {Spread} from 'lexical';
|
||||
|
||||
// Spread is a Typescript utility that allows us to spread the properties
|
||||
@ -249,21 +288,10 @@ export type SerializedTextNode = Spread<
|
||||
>;
|
||||
```
|
||||
|
||||
If we wanted to make changes to the above `TextNode`, we should be sure to not remove or change an existing property, as this can cause data corruption. Instead, opt to add the functionality as a new property field instead, and use the version to determine how to handle the differences in your node.
|
||||
If we wanted to make changes to the above `TextNode`, we should be sure to not remove or change an existing property, as this can cause data corruption. Instead, opt to add the functionality as a new optional property field instead.
|
||||
|
||||
```js
|
||||
export type SerializedTextNodeV1 = Spread<
|
||||
{
|
||||
detail: number;
|
||||
format: number;
|
||||
mode: TextModeType;
|
||||
style: string;
|
||||
text: string;
|
||||
},
|
||||
SerializedLexicalNode
|
||||
>;
|
||||
|
||||
export type SerializedTextNodeV2 = Spread<
|
||||
```ts
|
||||
export type SerializedTextNode = Spread<
|
||||
{
|
||||
detail: number;
|
||||
format: number;
|
||||
@ -271,15 +299,53 @@ export type SerializedTextNodeV2 = Spread<
|
||||
style: string;
|
||||
text: string;
|
||||
// Our new field we've added
|
||||
newField: string,
|
||||
// Notice the version is now 2
|
||||
version: 2,
|
||||
newField?: string,
|
||||
},
|
||||
SerializedLexicalNode
|
||||
>;
|
||||
|
||||
export type SerializedTextNode = SerializedTextNodeV1 | SerializedTextNodeV2;
|
||||
```
|
||||
|
||||
### Dangers of a flat version property
|
||||
|
||||
The `updateFromJSON` method should ignore `type` and `version`, to support subclassing and code re-use. Ideally, you should only evolve your types in a backwards compatible way (new fields are optional), and/or have a uniquely named property to store the version in your class. Generally speaking, it's best if nearly all properties are optional and the node provides defaults for each property. This allows you to write less boilerplate code and produce smaller JSON.
|
||||
|
||||
The reason that `version` is no longer recommended is that it does not compose with subclasses. Consider this hierarchy:
|
||||
|
||||
```ts
|
||||
class TextNode {
|
||||
exportJSON() {
|
||||
return { /* ... */, version: 1 };
|
||||
}
|
||||
}
|
||||
class ExtendedTextNode extends TextNode {
|
||||
exportJSON() {
|
||||
return { ...super.exportJSON() };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If `TextNode` is updated to `version: 2` then this version and new serialization will propagate to `ExtendedTextNode` via the `super.exportJSON()` call, but this leaves nowhere to store a version for `ExtendedTextNode` or vice versa. If the `ExtendedTextNode` explicitly specified a `version`, then the version of the base class will be ignored even though the representation of the JSON from the base class may change:
|
||||
|
||||
```ts
|
||||
class TextNode {
|
||||
exportJSON() {
|
||||
return { /* ... */, version: 2 };
|
||||
}
|
||||
}
|
||||
class ExtendedTextNode extends TextNode {
|
||||
exportJSON() {
|
||||
// The super's layout has changed, but the version information is lost
|
||||
return { ...super.exportJSON(), version: 1 };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
So then you have a situation where there are possibly two JSON layouts for `ExtendedTextNode` with the same version, because the base class version changed due to a package upgrade.
|
||||
|
||||
If you do have incompatible representations, it's probably best to choose a new type. This is basically the only way that will force old configurations to fail, as `importJSON` implementations often don't do runtime validation and dangerously assume that the values are the correct type.
|
||||
|
||||
There are other schemes that would allow for composable versions, such as nesting the superclass data, or choosing a different name for a version property in each subclass. In practice, explicit versioning is generally redundant if the serialization is properly parsed, so it is recommended that you use the simpler approach with a flat representation with mostly optional properties.
|
||||
|
||||
### Handling extended HTML styling
|
||||
|
||||
Since the TextNode is foundational to all Lexical packages, including the plain text use case. Handling any rich text logic is undesirable. This creates the need to override the TextNode to handle serialization and deserialization of HTML/CSS styling properties to achieve full fidelity between JSON \<-\> HTML. Since this is a very popular use case, below we are proving a recipe to handle the most common use cases.
|
||||
@ -364,7 +430,7 @@ export class ExtendedTextNode extends TextNode {
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedTextNode): TextNode {
|
||||
return TextNode.importJSON(serializedNode);
|
||||
return $createExtendedTextNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
isSimpleText() {
|
||||
@ -374,7 +440,7 @@ export class ExtendedTextNode extends TextNode {
|
||||
// no need to add exportJSON here, since we are not adding any new properties
|
||||
}
|
||||
|
||||
export function $createExtendedTextNode(text: string): ExtendedTextNode {
|
||||
export function $createExtendedTextNode(text: string = ''): ExtendedTextNode {
|
||||
return $applyNodeReplacement(new ExtendedTextNode(text));
|
||||
}
|
||||
|
||||
|
@ -69,7 +69,7 @@ export class EmojiNode extends TextNode {
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedEmojiNode): EmojiNode {
|
||||
return $createEmojiNode(serializedNode.unifiedID);
|
||||
return $createEmojiNode(serializedNode.unifiedID).updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
exportJSON(): SerializedEmojiNode {
|
||||
|
@ -393,6 +393,7 @@ declare export class LexicalNode {
|
||||
constructor(key?: NodeKey): void;
|
||||
exportDOM(editor: LexicalEditor): DOMExportOutput;
|
||||
exportJSON(): SerializedLexicalNode;
|
||||
updateFromJSON(serializedNode: $FlowFixMe): this;
|
||||
getType(): string;
|
||||
isAttached(): boolean;
|
||||
isSelected(): boolean;
|
||||
|
@ -23,6 +23,8 @@ import {
|
||||
// DOM
|
||||
export const DOM_ELEMENT_TYPE = 1;
|
||||
export const DOM_TEXT_TYPE = 3;
|
||||
export const DOM_DOCUMENT_TYPE = 9;
|
||||
export const DOM_DOCUMENT_FRAGMENT_TYPE = 11;
|
||||
|
||||
// Reconciling
|
||||
export const NO_DIRTY_NODES = 0;
|
||||
|
@ -40,7 +40,7 @@ import {
|
||||
markNodesWithTypesAsDirty,
|
||||
} from './LexicalUtils';
|
||||
import {ArtificialNode__DO_NOT_USE} from './nodes/ArtificialNode';
|
||||
import {DecoratorNode} from './nodes/LexicalDecoratorNode';
|
||||
import {$isDecoratorNode} from './nodes/LexicalDecoratorNode';
|
||||
import {LineBreakNode} from './nodes/LexicalLineBreakNode';
|
||||
import {ParagraphNode} from './nodes/LexicalParagraphNode';
|
||||
import {RootNode} from './nodes/LexicalRootNode';
|
||||
@ -498,7 +498,7 @@ export function createEditor(editorConfig?: CreateEditorArgs): LexicalEditor {
|
||||
`${name} should implement "importDOM" if using a custom "exportDOM" method to ensure HTML serialization (important for copy & paste) works as expected`,
|
||||
);
|
||||
}
|
||||
if (proto instanceof DecoratorNode) {
|
||||
if ($isDecoratorNode(proto)) {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (!proto.hasOwnProperty('decorate')) {
|
||||
console.warn(
|
||||
|
@ -59,7 +59,6 @@ import {
|
||||
KEY_TAB_COMMAND,
|
||||
MOVE_TO_END,
|
||||
MOVE_TO_START,
|
||||
ParagraphNode,
|
||||
PASTE_COMMAND,
|
||||
REDO_COMMAND,
|
||||
REMOVE_TEXT_COMMAND,
|
||||
@ -69,8 +68,6 @@ import {
|
||||
import {KEY_MODIFIER_COMMAND, SELECT_ALL_COMMAND} from './LexicalCommands';
|
||||
import {
|
||||
COMPOSITION_START_CHAR,
|
||||
DOM_ELEMENT_TYPE,
|
||||
DOM_TEXT_TYPE,
|
||||
DOUBLE_LINE_BREAK,
|
||||
IS_ALL_FORMATTING,
|
||||
} from './LexicalConstants';
|
||||
@ -92,6 +89,7 @@ import {
|
||||
doesContainGrapheme,
|
||||
getAnchorTextFromDOM,
|
||||
getDOMSelection,
|
||||
getDOMSelectionFromTarget,
|
||||
getDOMTextNode,
|
||||
getEditorPropertyFromDOMNode,
|
||||
getEditorsToPropagate,
|
||||
@ -109,8 +107,10 @@ import {
|
||||
isDeleteWordBackward,
|
||||
isDeleteWordForward,
|
||||
isDOMNode,
|
||||
isDOMTextNode,
|
||||
isEscape,
|
||||
isFirefoxClipboardEvents,
|
||||
isHTMLElement,
|
||||
isItalic,
|
||||
isLexicalEditor,
|
||||
isLineBreak,
|
||||
@ -254,9 +254,8 @@ function shouldSkipSelectionChange(
|
||||
offset: number,
|
||||
): boolean {
|
||||
return (
|
||||
domNode !== null &&
|
||||
isDOMTextNode(domNode) &&
|
||||
domNode.nodeValue !== null &&
|
||||
domNode.nodeType === DOM_TEXT_TYPE &&
|
||||
offset !== 0 &&
|
||||
offset !== domNode.nodeValue.length
|
||||
);
|
||||
@ -349,11 +348,15 @@ function onSelectionChange(
|
||||
selection.format = anchorNode.getFormat();
|
||||
selection.style = anchorNode.getStyle();
|
||||
} else if (anchor.type === 'element' && !isRootTextContentEmpty) {
|
||||
invariant(
|
||||
$isElementNode(anchorNode),
|
||||
'Point.getNode() must return ElementNode when type is element',
|
||||
);
|
||||
const lastNode = anchor.getNode();
|
||||
selection.style = '';
|
||||
if (
|
||||
lastNode instanceof ParagraphNode &&
|
||||
lastNode.getChildrenSize() === 0
|
||||
// This previously applied to all ParagraphNode
|
||||
lastNode.isEmpty()
|
||||
) {
|
||||
selection.format = lastNode.getTextFormat();
|
||||
selection.style = lastNode.getTextStyle();
|
||||
@ -455,13 +458,11 @@ function onClick(event: PointerEvent, editor: LexicalEditor): void {
|
||||
// This is used to update the selection on touch devices when the user clicks on text after a
|
||||
// node selection. See isSelectionChangeFromMouseDown for the inverse
|
||||
const domAnchorNode = domSelection.anchorNode;
|
||||
if (domAnchorNode !== null) {
|
||||
const nodeType = domAnchorNode.nodeType;
|
||||
// If the user is attempting to click selection back onto text, then
|
||||
// we should attempt create a range selection.
|
||||
// When we click on an empty paragraph node or the end of a paragraph that ends
|
||||
// with an image/poll, the nodeType will be ELEMENT_NODE
|
||||
if (nodeType === DOM_ELEMENT_TYPE || nodeType === DOM_TEXT_TYPE) {
|
||||
if (isHTMLElement(domAnchorNode) || isDOMTextNode(domAnchorNode)) {
|
||||
const newSelection = $internalCreateRangeSelection(
|
||||
lastSelection,
|
||||
domSelection,
|
||||
@ -472,7 +473,6 @@ function onClick(event: PointerEvent, editor: LexicalEditor): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchCommand(editor, CLICK_COMMAND, event);
|
||||
});
|
||||
@ -1133,14 +1133,7 @@ function getRootElementRemoveHandles(
|
||||
const activeNestedEditorsMap: Map<string, LexicalEditor> = new Map();
|
||||
|
||||
function onDocumentSelectionChange(event: Event): void {
|
||||
const target = event.target as null | Element | Document;
|
||||
const targetWindow =
|
||||
target == null
|
||||
? null
|
||||
: target.nodeType === 9
|
||||
? (target as Document).defaultView
|
||||
: (target as Element).ownerDocument.defaultView;
|
||||
const domSelection = getDOMSelection(targetWindow);
|
||||
const domSelection = getDOMSelectionFromTarget(event.target);
|
||||
if (domSelection === null) {
|
||||
return;
|
||||
}
|
||||
@ -1154,17 +1147,11 @@ function onDocumentSelectionChange(event: Event): void {
|
||||
updateEditor(nextActiveEditor, () => {
|
||||
const lastSelection = $getPreviousSelection();
|
||||
const domAnchorNode = domSelection.anchorNode;
|
||||
if (domAnchorNode === null) {
|
||||
return;
|
||||
}
|
||||
const nodeType = domAnchorNode.nodeType;
|
||||
if (isHTMLElement(domAnchorNode) || isDOMTextNode(domAnchorNode)) {
|
||||
// If the user is attempting to click selection back onto text, then
|
||||
// we should attempt create a range selection.
|
||||
// When we click on an empty paragraph node or the end of a paragraph that ends
|
||||
// with an image/poll, the nodeType will be ELEMENT_NODE
|
||||
if (nodeType !== DOM_ELEMENT_TYPE && nodeType !== DOM_TEXT_TYPE) {
|
||||
return;
|
||||
}
|
||||
const newSelection = $internalCreateRangeSelection(
|
||||
lastSelection,
|
||||
domSelection,
|
||||
@ -1172,6 +1159,7 @@ function onDocumentSelectionChange(event: Event): void {
|
||||
event,
|
||||
);
|
||||
$setSelection(newSelection);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,6 @@ import {
|
||||
$isTextNode,
|
||||
$setSelection,
|
||||
} from '.';
|
||||
import {DOM_TEXT_TYPE} from './LexicalConstants';
|
||||
import {updateEditor} from './LexicalUpdates';
|
||||
import {
|
||||
$getNodeByKey,
|
||||
@ -32,6 +31,7 @@ import {
|
||||
getParentElement,
|
||||
getWindow,
|
||||
internalGetRoot,
|
||||
isDOMTextNode,
|
||||
isDOMUnmanaged,
|
||||
isFirefoxClipboardEvents,
|
||||
isHTMLElement,
|
||||
@ -112,7 +112,7 @@ function shouldUpdateTextNodeFromMutation(
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return targetDOM.nodeType === DOM_TEXT_TYPE && targetNode.isAttached();
|
||||
return isDOMTextNode(targetDOM) && targetNode.isAttached();
|
||||
}
|
||||
|
||||
function $getNearestManagedNodePairFromDOMNode(
|
||||
@ -183,14 +183,10 @@ export function $flushMutations(
|
||||
if (
|
||||
shouldFlushTextMutations &&
|
||||
$isTextNode(targetNode) &&
|
||||
isDOMTextNode(targetDOM) &&
|
||||
shouldUpdateTextNodeFromMutation(selection, targetDOM, targetNode)
|
||||
) {
|
||||
$handleTextMutation(
|
||||
// nodeType === DOM_TEXT_TYPE is a Text DOM node
|
||||
targetDOM as Text,
|
||||
targetNode,
|
||||
editor,
|
||||
);
|
||||
$handleTextMutation(targetDOM, targetNode, editor);
|
||||
}
|
||||
} else if (type === 'childList') {
|
||||
shouldRevertSelection = true;
|
||||
|
@ -51,11 +51,24 @@ import {
|
||||
|
||||
export type NodeMap = Map<NodeKey, LexicalNode>;
|
||||
|
||||
/**
|
||||
* The base type for all serialized nodes
|
||||
*/
|
||||
export type SerializedLexicalNode = {
|
||||
/** The type string used by the Node class */
|
||||
type: string;
|
||||
/** A numeric version for this schema, defaulting to 1, but not generally recommended for use */
|
||||
version: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Omit the children, type, and version properties from the given SerializedLexicalNode definition.
|
||||
*/
|
||||
export type LexicalUpdateJSON<T extends SerializedLexicalNode> = Omit<
|
||||
T,
|
||||
'children' | 'type' | 'version'
|
||||
>;
|
||||
|
||||
/** @internal */
|
||||
export interface LexicalPrivateDOM {
|
||||
__lexicalTextContent?: string | undefined | null;
|
||||
@ -889,6 +902,41 @@ export class LexicalNode {
|
||||
this.name,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update this LexicalNode instance from serialized JSON. It's recommended
|
||||
* to implement as much logic as possible in this method instead of the
|
||||
* static importJSON method, so that the functionality can be inherited in subclasses.
|
||||
*
|
||||
* The LexicalUpdateJSON utility type should be used to ignore any type, version,
|
||||
* or children properties in the JSON so that the extended JSON from subclasses
|
||||
* are acceptable parameters for the super call.
|
||||
*
|
||||
* If overridden, this method must call super.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* class MyTextNode extends TextNode {
|
||||
* // ...
|
||||
* static importJSON(serializedNode: SerializedMyTextNode): MyTextNode {
|
||||
* return $createMyTextNode()
|
||||
* .updateFromJSON(serializedNode);
|
||||
* }
|
||||
* updateFromJSON(
|
||||
* serializedNode: LexicalUpdateJSON<SerializedMyTextNode>,
|
||||
* ): this {
|
||||
* return super.updateFromJSON(serializedNode)
|
||||
* .setMyProperty(serializedNode.myProperty);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
**/
|
||||
updateFromJSON(
|
||||
serializedNode: LexicalUpdateJSON<SerializedLexicalNode>,
|
||||
): this {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*
|
||||
|
@ -23,7 +23,6 @@ import {
|
||||
$isDecoratorNode,
|
||||
$isElementNode,
|
||||
$isLineBreakNode,
|
||||
$isParagraphNode,
|
||||
$isRootNode,
|
||||
$isTextNode,
|
||||
} from '.';
|
||||
@ -332,9 +331,8 @@ function reconcileElementTerminatingLineBreak(
|
||||
}
|
||||
}
|
||||
|
||||
function reconcileParagraphFormat(element: ElementNode): void {
|
||||
function reconcileTextFormat(element: ElementNode): void {
|
||||
if (
|
||||
$isParagraphNode(element) &&
|
||||
subTreeTextFormat != null &&
|
||||
subTreeTextFormat !== element.__textFormat &&
|
||||
!activeEditorStateReadOnly
|
||||
@ -344,9 +342,8 @@ function reconcileParagraphFormat(element: ElementNode): void {
|
||||
}
|
||||
}
|
||||
|
||||
function reconcileParagraphStyle(element: ElementNode): void {
|
||||
function reconcileTextStyle(element: ElementNode): void {
|
||||
if (
|
||||
$isParagraphNode(element) &&
|
||||
subTreeTextStyle !== '' &&
|
||||
subTreeTextStyle !== element.__textStyle &&
|
||||
!activeEditorStateReadOnly
|
||||
@ -438,8 +435,8 @@ function $reconcileChildrenWithDirection(
|
||||
subTreeTextStyle = '';
|
||||
$reconcileChildren(prevElement, nextElement, nextElement.getDOMSlot(dom));
|
||||
reconcileBlockDirection(nextElement, dom);
|
||||
reconcileParagraphFormat(nextElement);
|
||||
reconcileParagraphStyle(nextElement);
|
||||
reconcileTextFormat(nextElement);
|
||||
reconcileTextStyle(nextElement);
|
||||
subTreeDirectionedTextContent = previousSubTreeDirectionTextContent;
|
||||
}
|
||||
|
||||
|
@ -21,14 +21,13 @@ import {
|
||||
$isDecoratorNode,
|
||||
$isElementNode,
|
||||
$isLineBreakNode,
|
||||
$isParagraphNode,
|
||||
$isRootNode,
|
||||
$isTextNode,
|
||||
$setSelection,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
TextNode,
|
||||
} from '.';
|
||||
import {DOM_ELEMENT_TYPE, TEXT_TYPE_TO_FORMAT} from './LexicalConstants';
|
||||
import {TEXT_TYPE_TO_FORMAT} from './LexicalConstants';
|
||||
import {
|
||||
markCollapsedSelectionFormat,
|
||||
markSelectionChangeFromDOMUpdate,
|
||||
@ -57,6 +56,7 @@ import {
|
||||
getElementByKeyOrThrow,
|
||||
getTextNodeOffset,
|
||||
INTERNAL_$isBlock,
|
||||
isHTMLElement,
|
||||
isSelectionCapturedInDecoratorInput,
|
||||
isSelectionWithinEditor,
|
||||
removeDOMBlockCursorElement,
|
||||
@ -1225,9 +1225,9 @@ export class RangeSelection implements BaseSelection {
|
||||
selectedTextNodes.push(selectedNode);
|
||||
}
|
||||
}
|
||||
const applyFormatToParagraphs = (alignWith: number | null) => {
|
||||
const applyFormatToElements = (alignWith: number | null) => {
|
||||
selectedNodes.forEach((node) => {
|
||||
if ($isParagraphNode(node)) {
|
||||
if ($isElementNode(node)) {
|
||||
const newFormat = node.getFormatFlags(formatType, alignWith);
|
||||
node.setTextFormat(newFormat);
|
||||
}
|
||||
@ -1239,7 +1239,7 @@ export class RangeSelection implements BaseSelection {
|
||||
this.toggleFormat(formatType);
|
||||
// When changing format, we should stop composition
|
||||
$setCompositionKey(null);
|
||||
applyFormatToParagraphs(alignWithFormat);
|
||||
applyFormatToElements(alignWithFormat);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1271,7 +1271,7 @@ export class RangeSelection implements BaseSelection {
|
||||
formatType,
|
||||
alignWithFormat,
|
||||
);
|
||||
applyFormatToParagraphs(firstNextFormat);
|
||||
applyFormatToElements(firstNextFormat);
|
||||
|
||||
const lastIndex = selectedTextNodesLength - 1;
|
||||
let lastNode = selectedTextNodes[lastIndex];
|
||||
@ -2086,7 +2086,7 @@ function $internalResolveSelectionPoint(
|
||||
// need to figure out (using the offset) what text
|
||||
// node should be selected.
|
||||
|
||||
if (dom.nodeType === DOM_ELEMENT_TYPE) {
|
||||
if (isHTMLElement(dom)) {
|
||||
// Resolve element to a ElementNode, or TextNode, or null
|
||||
let moveSelectionToEnd = false;
|
||||
// Given we're moving selection to another node, selection is
|
||||
@ -2924,7 +2924,7 @@ export function updateDOMSelection(
|
||||
rootElement === document.activeElement
|
||||
) {
|
||||
const selectionTarget: null | Range | HTMLElement | Text =
|
||||
nextSelection instanceof RangeSelection &&
|
||||
$isRangeSelection(nextSelection) &&
|
||||
nextSelection.anchor.type === 'element'
|
||||
? (nextAnchorNode.childNodes[nextAnchorOffset] as HTMLElement | Text) ||
|
||||
null
|
||||
|
@ -55,6 +55,9 @@ import {
|
||||
} from '.';
|
||||
import {
|
||||
COMPOSITION_SUFFIX,
|
||||
DOM_DOCUMENT_FRAGMENT_TYPE,
|
||||
DOM_DOCUMENT_TYPE,
|
||||
DOM_ELEMENT_TYPE,
|
||||
DOM_TEXT_TYPE,
|
||||
HAS_DIRTY_NODES,
|
||||
LTR_REGEX,
|
||||
@ -193,8 +196,20 @@ export function $isTokenOrSegmented(node: TextNode): boolean {
|
||||
return node.isToken() || node.isSegmented();
|
||||
}
|
||||
|
||||
export function isDOMTextNode(node: Node): node is Text {
|
||||
return node.nodeType === DOM_TEXT_TYPE;
|
||||
/**
|
||||
* @param node - The element being tested
|
||||
* @returns Returns true if node is an DOM Text node, false otherwise.
|
||||
*/
|
||||
export function isDOMTextNode(node: unknown): node is Text {
|
||||
return isDOMNode(node) && node.nodeType === DOM_TEXT_TYPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param node - The element being tested
|
||||
* @returns Returns true if node is an DOM Document node, false otherwise.
|
||||
*/
|
||||
export function isDOMDocumentNode(node: unknown): node is Document {
|
||||
return isDOMNode(node) && node.nodeType === DOM_DOCUMENT_TYPE;
|
||||
}
|
||||
|
||||
export function getDOMTextNode(element: Node | null): Text | null {
|
||||
@ -633,10 +648,7 @@ export function createUID(): string {
|
||||
}
|
||||
|
||||
export function getAnchorTextFromDOM(anchorNode: Node): null | string {
|
||||
if (anchorNode.nodeType === DOM_TEXT_TYPE) {
|
||||
return anchorNode.nodeValue;
|
||||
}
|
||||
return null;
|
||||
return isDOMTextNode(anchorNode) ? anchorNode.nodeValue : null;
|
||||
}
|
||||
|
||||
export function $updateSelectedTextFromDOM(
|
||||
@ -1304,15 +1316,25 @@ export function getParentElement(node: Node): HTMLElement | null {
|
||||
: parentElement;
|
||||
}
|
||||
|
||||
export function getDOMOwnerDocument(
|
||||
target: EventTarget | null,
|
||||
): Document | null {
|
||||
return isDOMDocumentNode(target)
|
||||
? target
|
||||
: isHTMLElement(target)
|
||||
? target.ownerDocument
|
||||
: null;
|
||||
}
|
||||
|
||||
export function scrollIntoViewIfNeeded(
|
||||
editor: LexicalEditor,
|
||||
selectionRect: DOMRect,
|
||||
rootElement: HTMLElement,
|
||||
): void {
|
||||
const doc = rootElement.ownerDocument;
|
||||
const defaultView = doc.defaultView;
|
||||
const doc = getDOMOwnerDocument(rootElement);
|
||||
const defaultView = getDefaultView(doc);
|
||||
|
||||
if (defaultView === null) {
|
||||
if (doc === null || defaultView === null) {
|
||||
return;
|
||||
}
|
||||
let {top: currentTop, bottom: currentBottom} = selectionRect;
|
||||
@ -1414,9 +1436,9 @@ export function $hasAncestor(
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getDefaultView(domElem: HTMLElement): Window | null {
|
||||
const ownerDoc = domElem.ownerDocument;
|
||||
return (ownerDoc && ownerDoc.defaultView) || null;
|
||||
export function getDefaultView(domElem: EventTarget | null): Window | null {
|
||||
const ownerDoc = getDOMOwnerDocument(domElem);
|
||||
return ownerDoc ? ownerDoc.defaultView : null;
|
||||
}
|
||||
|
||||
export function getWindow(editor: LexicalEditor): Window {
|
||||
@ -1658,6 +1680,19 @@ export function getDOMSelection(targetWindow: null | Window): null | Selection {
|
||||
return !CAN_USE_DOM ? null : (targetWindow || window).getSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the selection for the defaultView of the ownerDocument of given EventTarget.
|
||||
*
|
||||
* @param eventTarget The node to get the selection from
|
||||
* @returns a Selection or null
|
||||
*/
|
||||
export function getDOMSelectionFromTarget(
|
||||
eventTarget: null | EventTarget,
|
||||
): null | Selection {
|
||||
const defaultView = getDefaultView(eventTarget);
|
||||
return defaultView ? defaultView.getSelection() : null;
|
||||
}
|
||||
|
||||
export function $splitNode(
|
||||
node: ElementNode,
|
||||
offset: number,
|
||||
@ -1736,7 +1771,7 @@ export function isHTMLAnchorElement(x: unknown): x is HTMLAnchorElement {
|
||||
* @returns Returns true if x is an HTML element, false otherwise.
|
||||
*/
|
||||
export function isHTMLElement(x: unknown): x is HTMLElement {
|
||||
return isDOMNode(x) && x.nodeType === 1;
|
||||
return isDOMNode(x) && x.nodeType === DOM_ELEMENT_TYPE;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1757,7 +1792,7 @@ export function isDOMNode(x: unknown): x is Node {
|
||||
* @returns Returns true if x is a document fragment, false otherwise.
|
||||
*/
|
||||
export function isDocumentFragment(x: unknown): x is DocumentFragment {
|
||||
return isDOMNode(x) && x.nodeType === 11;
|
||||
return isDOMNode(x) && x.nodeType === DOM_DOCUMENT_FRAGMENT_TYPE;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -63,6 +63,7 @@ import invariant from 'shared/invariant';
|
||||
import * as ReactTestUtils from 'shared/react-test-utils';
|
||||
|
||||
import {emptyFunction} from '../../LexicalUtils';
|
||||
import {SerializedParagraphNode} from '../../nodes/LexicalParagraphNode';
|
||||
import {
|
||||
$createTestDecoratorNode,
|
||||
$createTestElementNode,
|
||||
@ -1027,7 +1028,7 @@ describe('LexicalEditor tests', () => {
|
||||
editable ? 'editable' : 'non-editable'
|
||||
})`, async () => {
|
||||
const JSON_EDITOR_STATE =
|
||||
'{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"123","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"root","version":1}}';
|
||||
'{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"123","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"root","version":1}}';
|
||||
init();
|
||||
const contentEditable = editor.getRootElement();
|
||||
editor.setEditable(editable);
|
||||
@ -1194,6 +1195,8 @@ describe('LexicalEditor tests', () => {
|
||||
__prev: null,
|
||||
__size: 1,
|
||||
__style: '',
|
||||
__textFormat: 0,
|
||||
__textStyle: '',
|
||||
__type: 'root',
|
||||
});
|
||||
expect(paragraph).toEqual({
|
||||
@ -1281,6 +1284,8 @@ describe('LexicalEditor tests', () => {
|
||||
__prev: null,
|
||||
__size: 1,
|
||||
__style: '',
|
||||
__textFormat: 0,
|
||||
__textStyle: '',
|
||||
__type: 'root',
|
||||
});
|
||||
expect(parsedParagraph).toEqual({
|
||||
@ -1363,6 +1368,8 @@ describe('LexicalEditor tests', () => {
|
||||
__prev: null,
|
||||
__size: 1,
|
||||
__style: '',
|
||||
__textFormat: 0,
|
||||
__textStyle: '',
|
||||
__type: 'root',
|
||||
});
|
||||
expect(parsedParagraph).toEqual({
|
||||
@ -2775,8 +2782,8 @@ describe('LexicalEditor tests', () => {
|
||||
return new CustomParagraphNode(node.__key);
|
||||
}
|
||||
|
||||
static importJSON() {
|
||||
return new CustomParagraphNode();
|
||||
static importJSON(serializedNode: SerializedParagraphNode) {
|
||||
return new CustomParagraphNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,6 +63,8 @@ describe('LexicalEditorState tests', () => {
|
||||
__prev: null,
|
||||
__size: 1,
|
||||
__style: '',
|
||||
__textFormat: 0,
|
||||
__textStyle: '',
|
||||
__type: 'root',
|
||||
});
|
||||
expect(paragraph).toEqual({
|
||||
@ -149,6 +151,8 @@ describe('LexicalEditorState tests', () => {
|
||||
__prev: null,
|
||||
__size: 0,
|
||||
__style: '',
|
||||
__textFormat: 0,
|
||||
__textStyle: '',
|
||||
__type: 'root',
|
||||
},
|
||||
],
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
NodeKey,
|
||||
ParagraphNode,
|
||||
RangeSelection,
|
||||
SerializedLexicalNode,
|
||||
SerializedTextNode,
|
||||
TextNode,
|
||||
} from 'lexical';
|
||||
@ -48,8 +49,8 @@ class TestNode extends LexicalNode {
|
||||
return document.createElement('div');
|
||||
}
|
||||
|
||||
static importJSON() {
|
||||
return new TestNode();
|
||||
static importJSON(serializedNode: SerializedLexicalNode) {
|
||||
return new TestNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,8 +63,8 @@ class InlineDecoratorNode extends DecoratorNode<string> {
|
||||
return new InlineDecoratorNode();
|
||||
}
|
||||
|
||||
static importJSON() {
|
||||
return new InlineDecoratorNode();
|
||||
static importJSON(serializedNode: SerializedLexicalNode) {
|
||||
return new InlineDecoratorNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
createDOM(): HTMLElement {
|
||||
|
@ -24,6 +24,7 @@ import {
|
||||
TextNode,
|
||||
} from 'lexical';
|
||||
|
||||
import {SerializedElementNode} from '../..';
|
||||
import {$assertRangeSelection, initializeUnitTest, invariant} from '../utils';
|
||||
|
||||
describe('LexicalSelection tests', () => {
|
||||
@ -801,8 +802,8 @@ describe('Regression tests for #6701', () => {
|
||||
static getType() {
|
||||
return 'inline-element-node';
|
||||
}
|
||||
static importJSON() {
|
||||
return new InlineElementNode();
|
||||
static importJSON(serializedNode: SerializedElementNode) {
|
||||
return new InlineElementNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
isInline() {
|
||||
return true;
|
||||
|
File diff suppressed because one or more lines are too long
@ -373,20 +373,12 @@ describe('$applyNodeReplacement', () => {
|
||||
return new ExtendedTextNode(node.__text, node.getKey());
|
||||
}
|
||||
initWithTextNode(node: TextNode): this {
|
||||
this.__text = node.__text;
|
||||
TextNode.prototype.afterCloneFrom.call(this, node);
|
||||
return this;
|
||||
}
|
||||
initWithJSON(serializedNode: SerializedTextNode): this {
|
||||
this.setTextContent(serializedNode.text);
|
||||
this.setFormat(serializedNode.format);
|
||||
this.setDetail(serializedNode.detail);
|
||||
this.setMode(serializedNode.mode);
|
||||
this.setStyle(serializedNode.style);
|
||||
return this;
|
||||
const self = this.getWritable();
|
||||
TextNode.prototype.updateFromJSON.call(self, node.exportJSON());
|
||||
return self;
|
||||
}
|
||||
static importJSON(serializedNode: SerializedTextNode): ExtendedTextNode {
|
||||
return $createExtendedTextNode().initWithJSON(serializedNode);
|
||||
return $createExtendedTextNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
}
|
||||
class ExtendedExtendedTextNode extends ExtendedTextNode {
|
||||
@ -402,7 +394,7 @@ describe('$applyNodeReplacement', () => {
|
||||
static importJSON(
|
||||
serializedNode: SerializedTextNode,
|
||||
): ExtendedExtendedTextNode {
|
||||
return $createExtendedExtendedTextNode().initWithJSON(serializedNode);
|
||||
return $createExtendedExtendedTextNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
}
|
||||
function $createExtendedTextNode(text: string = '') {
|
||||
|
@ -175,11 +175,7 @@ export class TestElementNode extends ElementNode {
|
||||
static importJSON(
|
||||
serializedNode: SerializedTestElementNode,
|
||||
): TestInlineElementNode {
|
||||
const node = $createTestInlineElementNode();
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setIndent(serializedNode.indent);
|
||||
node.setDirection(serializedNode.direction);
|
||||
return node;
|
||||
return $createTestInlineElementNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
createDOM() {
|
||||
@ -207,7 +203,7 @@ export class TestTextNode extends TextNode {
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedTestTextNode): TestTextNode {
|
||||
return new TestTextNode(serializedNode.text);
|
||||
return new TestTextNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
}
|
||||
|
||||
@ -225,11 +221,7 @@ export class TestInlineElementNode extends ElementNode {
|
||||
static importJSON(
|
||||
serializedNode: SerializedTestInlineElementNode,
|
||||
): TestInlineElementNode {
|
||||
const node = $createTestInlineElementNode();
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setIndent(serializedNode.indent);
|
||||
node.setDirection(serializedNode.direction);
|
||||
return node;
|
||||
return $createTestInlineElementNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
createDOM() {
|
||||
@ -263,11 +255,7 @@ export class TestShadowRootNode extends ElementNode {
|
||||
static importJSON(
|
||||
serializedNode: SerializedTestShadowRootNode,
|
||||
): TestShadowRootNode {
|
||||
const node = $createTestShadowRootNode();
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setIndent(serializedNode.indent);
|
||||
node.setDirection(serializedNode.direction);
|
||||
return node;
|
||||
return $createTestShadowRootNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
createDOM() {
|
||||
@ -301,16 +289,11 @@ export class TestSegmentedNode extends TextNode {
|
||||
static importJSON(
|
||||
serializedNode: SerializedTestSegmentedNode,
|
||||
): TestSegmentedNode {
|
||||
const node = $createTestSegmentedNode(serializedNode.text);
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setDetail(serializedNode.detail);
|
||||
node.setMode(serializedNode.mode);
|
||||
node.setStyle(serializedNode.style);
|
||||
return node;
|
||||
return $createTestSegmentedNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
}
|
||||
|
||||
export function $createTestSegmentedNode(text: string): TestSegmentedNode {
|
||||
export function $createTestSegmentedNode(text: string = ''): TestSegmentedNode {
|
||||
return new TestSegmentedNode(text).setMode('segmented');
|
||||
}
|
||||
|
||||
@ -328,11 +311,9 @@ export class TestExcludeFromCopyElementNode extends ElementNode {
|
||||
static importJSON(
|
||||
serializedNode: SerializedTestExcludeFromCopyElementNode,
|
||||
): TestExcludeFromCopyElementNode {
|
||||
const node = $createTestExcludeFromCopyElementNode();
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setIndent(serializedNode.indent);
|
||||
node.setDirection(serializedNode.direction);
|
||||
return node;
|
||||
return $createTestExcludeFromCopyElementNode().updateFromJSON(
|
||||
serializedNode,
|
||||
);
|
||||
}
|
||||
|
||||
createDOM() {
|
||||
@ -366,7 +347,7 @@ export class TestDecoratorNode extends DecoratorNode<JSX.Element> {
|
||||
static importJSON(
|
||||
serializedNode: SerializedTestDecoratorNode,
|
||||
): TestDecoratorNode {
|
||||
return $createTestDecoratorNode();
|
||||
return $createTestDecoratorNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
static importDOM() {
|
||||
|
@ -45,6 +45,7 @@ export type {
|
||||
DOMExportOutput,
|
||||
DOMExportOutputMap,
|
||||
LexicalNode,
|
||||
LexicalUpdateJSON,
|
||||
NodeKey,
|
||||
NodeMap,
|
||||
SerializedLexicalNode,
|
||||
@ -180,12 +181,15 @@ export {
|
||||
$setCompositionKey,
|
||||
$setSelection,
|
||||
$splitNode,
|
||||
getDOMOwnerDocument,
|
||||
getDOMSelection,
|
||||
getDOMSelectionFromTarget,
|
||||
getDOMTextNode,
|
||||
getEditorPropertyFromDOMNode,
|
||||
getNearestEditorFromDOMNode,
|
||||
isBlockDomNode,
|
||||
isDocumentFragment,
|
||||
isDOMDocumentNode,
|
||||
isDOMNode,
|
||||
isDOMTextNode,
|
||||
isDOMUnmanaged,
|
||||
|
@ -7,7 +7,6 @@
|
||||
*/
|
||||
|
||||
import type {KlassConstructor, LexicalEditor} from '../LexicalEditor';
|
||||
import type {NodeKey} from '../LexicalNode';
|
||||
import type {ElementNode} from './LexicalElementNode';
|
||||
|
||||
import {EditorConfig} from 'lexical';
|
||||
@ -25,9 +24,6 @@ export interface DecoratorNode<T> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
||||
export class DecoratorNode<T> extends LexicalNode {
|
||||
['constructor']!: KlassConstructor<typeof DecoratorNode<T>>;
|
||||
constructor(key?: NodeKey) {
|
||||
super(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* The returned value is added to the LexicalEditor._decorators
|
||||
|
@ -17,7 +17,13 @@ import type {
|
||||
PointType,
|
||||
RangeSelection,
|
||||
} from '../LexicalSelection';
|
||||
import type {KlassConstructor, LexicalEditor, Spread} from 'lexical';
|
||||
import type {
|
||||
KlassConstructor,
|
||||
LexicalEditor,
|
||||
LexicalUpdateJSON,
|
||||
Spread,
|
||||
TextFormatType,
|
||||
} from 'lexical';
|
||||
|
||||
import {IS_IOS, IS_SAFARI} from 'shared/environment';
|
||||
import invariant from 'shared/invariant';
|
||||
@ -27,6 +33,7 @@ import {
|
||||
DOUBLE_LINE_BREAK,
|
||||
ELEMENT_FORMAT_TO_TYPE,
|
||||
ELEMENT_TYPE_TO_FORMAT,
|
||||
TEXT_TYPE_TO_FORMAT,
|
||||
} from '../LexicalConstants';
|
||||
import {LexicalNode} from '../LexicalNode';
|
||||
import {
|
||||
@ -41,6 +48,7 @@ import {
|
||||
$isRootOrShadowRoot,
|
||||
isHTMLElement,
|
||||
removeFromParent,
|
||||
toggleTextFormatType,
|
||||
} from '../LexicalUtils';
|
||||
|
||||
export type SerializedElementNode<
|
||||
@ -51,6 +59,8 @@ export type SerializedElementNode<
|
||||
direction: 'ltr' | 'rtl' | null;
|
||||
format: ElementFormatType;
|
||||
indent: number;
|
||||
textFormat?: number;
|
||||
textStyle?: string;
|
||||
},
|
||||
SerializedLexicalNode
|
||||
>;
|
||||
@ -307,6 +317,10 @@ export class ElementNode extends LexicalNode {
|
||||
__indent: number;
|
||||
/** @internal */
|
||||
__dir: 'ltr' | 'rtl' | null;
|
||||
/** @internal */
|
||||
__textFormat: number;
|
||||
/** @internal */
|
||||
__textStyle: string;
|
||||
|
||||
constructor(key?: NodeKey) {
|
||||
super(key);
|
||||
@ -317,6 +331,8 @@ export class ElementNode extends LexicalNode {
|
||||
this.__style = '';
|
||||
this.__indent = 0;
|
||||
this.__dir = null;
|
||||
this.__textFormat = 0;
|
||||
this.__textStyle = '';
|
||||
}
|
||||
|
||||
afterCloneFrom(prevNode: this) {
|
||||
@ -328,6 +344,8 @@ export class ElementNode extends LexicalNode {
|
||||
this.__format = prevNode.__format;
|
||||
this.__style = prevNode.__style;
|
||||
this.__dir = prevNode.__dir;
|
||||
this.__textFormat = prevNode.__textFormat;
|
||||
this.__textStyle = prevNode.__textStyle;
|
||||
}
|
||||
|
||||
getFormat(): number {
|
||||
@ -527,6 +545,10 @@ export class ElementNode extends LexicalNode {
|
||||
const self = this.getLatest();
|
||||
return self.__dir;
|
||||
}
|
||||
getTextFormat(): number {
|
||||
const self = this.getLatest();
|
||||
return self.__textFormat;
|
||||
}
|
||||
hasFormat(type: ElementFormatType): boolean {
|
||||
if (type !== '') {
|
||||
const formatFlag = ELEMENT_TYPE_TO_FORMAT[type];
|
||||
@ -534,6 +556,25 @@ export class ElementNode extends LexicalNode {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
hasTextFormat(type: TextFormatType): boolean {
|
||||
const formatFlag = TEXT_TYPE_TO_FORMAT[type];
|
||||
return (this.getTextFormat() & formatFlag) !== 0;
|
||||
}
|
||||
/**
|
||||
* Returns the format flags applied to the node as a 32-bit integer.
|
||||
*
|
||||
* @returns a number representing the TextFormatTypes applied to the node.
|
||||
*/
|
||||
getFormatFlags(type: TextFormatType, alignWithFormat: null | number): number {
|
||||
const self = this.getLatest();
|
||||
const format = self.__textFormat;
|
||||
return toggleTextFormatType(format, type, alignWithFormat);
|
||||
}
|
||||
|
||||
getTextStyle(): string {
|
||||
const self = this.getLatest();
|
||||
return self.__textStyle;
|
||||
}
|
||||
|
||||
// Mutators
|
||||
|
||||
@ -614,6 +655,16 @@ export class ElementNode extends LexicalNode {
|
||||
self.__style = style || '';
|
||||
return this;
|
||||
}
|
||||
setTextFormat(type: number): this {
|
||||
const self = this.getWritable();
|
||||
self.__textFormat = type;
|
||||
return self;
|
||||
}
|
||||
setTextStyle(style: string): this {
|
||||
const self = this.getWritable();
|
||||
self.__textStyle = style;
|
||||
return self;
|
||||
}
|
||||
setIndent(indentLevel: number): this {
|
||||
const self = this.getWritable();
|
||||
self.__indent = indentLevel;
|
||||
@ -787,7 +838,7 @@ export class ElementNode extends LexicalNode {
|
||||
}
|
||||
// JSON serialization
|
||||
exportJSON(): SerializedElementNode {
|
||||
return {
|
||||
const json: SerializedElementNode = {
|
||||
children: [],
|
||||
direction: this.getDirection(),
|
||||
format: this.getFormatType(),
|
||||
@ -797,6 +848,26 @@ export class ElementNode extends LexicalNode {
|
||||
// that use the serialized string representation.
|
||||
...super.exportJSON(),
|
||||
};
|
||||
const textFormat = this.getTextFormat();
|
||||
const textStyle = this.getTextStyle();
|
||||
if (textFormat !== 0) {
|
||||
json.textFormat = textFormat;
|
||||
}
|
||||
if (textStyle !== '') {
|
||||
json.textStyle = textStyle;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
updateFromJSON(
|
||||
serializedNode: LexicalUpdateJSON<SerializedElementNode>,
|
||||
): this {
|
||||
return super
|
||||
.updateFromJSON(serializedNode)
|
||||
.setFormat(serializedNode.format)
|
||||
.setIndent(serializedNode.indent)
|
||||
.setDirection(serializedNode.direction)
|
||||
.setTextFormat(serializedNode.textFormat || 0)
|
||||
.setTextStyle(serializedNode.textStyle || '');
|
||||
}
|
||||
// These are intended to be extends for specific element heuristics.
|
||||
insertNewAfter(
|
||||
|
@ -14,9 +14,12 @@ import type {
|
||||
SerializedLexicalNode,
|
||||
} from '../LexicalNode';
|
||||
|
||||
import {DOM_TEXT_TYPE} from '../LexicalConstants';
|
||||
import {LexicalNode} from '../LexicalNode';
|
||||
import {$applyNodeReplacement, isBlockDomNode} from '../LexicalUtils';
|
||||
import {
|
||||
$applyNodeReplacement,
|
||||
isBlockDomNode,
|
||||
isDOMTextNode,
|
||||
} from '../LexicalUtils';
|
||||
|
||||
export type SerializedLineBreakNode = SerializedLexicalNode;
|
||||
|
||||
@ -64,7 +67,7 @@ export class LineBreakNode extends LexicalNode {
|
||||
static importJSON(
|
||||
serializedLineBreakNode: SerializedLineBreakNode,
|
||||
): LineBreakNode {
|
||||
return $createLineBreakNode();
|
||||
return $createLineBreakNode().updateFromJSON(serializedLineBreakNode);
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,8 +131,5 @@ function isLastChildInBlockNode(node: Node): boolean {
|
||||
}
|
||||
|
||||
function isWhitespaceDomTextNode(node: Node): boolean {
|
||||
return (
|
||||
node.nodeType === DOM_TEXT_TYPE &&
|
||||
/^( |\t|\r?\n)+$/.test(node.textContent || '')
|
||||
);
|
||||
return isDOMTextNode(node) && /^( |\t|\r?\n)+$/.test(node.textContent || '');
|
||||
}
|
||||
|
@ -17,24 +17,21 @@ import type {
|
||||
DOMConversionOutput,
|
||||
DOMExportOutput,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
} from '../LexicalNode';
|
||||
import type {RangeSelection} from '../LexicalSelection';
|
||||
import type {
|
||||
ElementFormatType,
|
||||
SerializedElementNode,
|
||||
} from './LexicalElementNode';
|
||||
import type {RangeSelection} from 'lexical';
|
||||
|
||||
import {TEXT_TYPE_TO_FORMAT} from '../LexicalConstants';
|
||||
import {
|
||||
$applyNodeReplacement,
|
||||
getCachedClassNameArray,
|
||||
isHTMLElement,
|
||||
setNodeIndentFromDOM,
|
||||
toggleTextFormatType,
|
||||
} from '../LexicalUtils';
|
||||
import {ElementNode} from './LexicalElementNode';
|
||||
import {$isTextNode, TextFormatType} from './LexicalTextNode';
|
||||
import {$isTextNode} from './LexicalTextNode';
|
||||
|
||||
export type SerializedParagraphNode = Spread<
|
||||
{
|
||||
@ -47,68 +44,15 @@ export type SerializedParagraphNode = Spread<
|
||||
/** @noInheritDoc */
|
||||
export class ParagraphNode extends ElementNode {
|
||||
['constructor']!: KlassConstructor<typeof ParagraphNode>;
|
||||
/** @internal */
|
||||
__textFormat: number;
|
||||
__textStyle: string;
|
||||
|
||||
constructor(key?: NodeKey) {
|
||||
super(key);
|
||||
this.__textFormat = 0;
|
||||
this.__textStyle = '';
|
||||
}
|
||||
|
||||
static getType(): string {
|
||||
return 'paragraph';
|
||||
}
|
||||
|
||||
getTextFormat(): number {
|
||||
const self = this.getLatest();
|
||||
return self.__textFormat;
|
||||
}
|
||||
|
||||
setTextFormat(type: number): this {
|
||||
const self = this.getWritable();
|
||||
self.__textFormat = type;
|
||||
return self;
|
||||
}
|
||||
|
||||
hasTextFormat(type: TextFormatType): boolean {
|
||||
const formatFlag = TEXT_TYPE_TO_FORMAT[type];
|
||||
return (this.getTextFormat() & formatFlag) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the format flags applied to the node as a 32-bit integer.
|
||||
*
|
||||
* @returns a number representing the TextFormatTypes applied to the node.
|
||||
*/
|
||||
getFormatFlags(type: TextFormatType, alignWithFormat: null | number): number {
|
||||
const self = this.getLatest();
|
||||
const format = self.__textFormat;
|
||||
return toggleTextFormatType(format, type, alignWithFormat);
|
||||
}
|
||||
|
||||
getTextStyle(): string {
|
||||
const self = this.getLatest();
|
||||
return self.__textStyle;
|
||||
}
|
||||
|
||||
setTextStyle(style: string): this {
|
||||
const self = this.getWritable();
|
||||
self.__textStyle = style;
|
||||
return self;
|
||||
}
|
||||
|
||||
static clone(node: ParagraphNode): ParagraphNode {
|
||||
return new ParagraphNode(node.__key);
|
||||
}
|
||||
|
||||
afterCloneFrom(prevNode: this) {
|
||||
super.afterCloneFrom(prevNode);
|
||||
this.__textFormat = prevNode.__textFormat;
|
||||
this.__textStyle = prevNode.__textStyle;
|
||||
}
|
||||
|
||||
// View
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
@ -160,17 +104,13 @@ 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);
|
||||
node.setTextFormat(serializedNode.textFormat);
|
||||
return node;
|
||||
return $createParagraphNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
exportJSON(): SerializedParagraphNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
// These are included explicitly for backwards compatibility
|
||||
textFormat: this.getTextFormat(),
|
||||
textStyle: this.getTextStyle(),
|
||||
};
|
||||
|
@ -98,11 +98,7 @@ export class RootNode extends ElementNode {
|
||||
|
||||
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;
|
||||
return $getRoot().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
collapseAtStart(): true {
|
||||
|
@ -33,12 +33,6 @@ export class TabNode extends TextNode {
|
||||
return new TabNode(node.__key);
|
||||
}
|
||||
|
||||
afterCloneFrom(prevNode: this): void {
|
||||
super.afterCloneFrom(prevNode);
|
||||
// TabNode __text can be either '\t' or ''. insertText will remove the empty Node
|
||||
this.__text = prevNode.__text;
|
||||
}
|
||||
|
||||
constructor(key?: NodeKey) {
|
||||
super('\t', key);
|
||||
this.__detail = IS_UNMERGEABLE;
|
||||
@ -60,22 +54,25 @@ export class TabNode extends TextNode {
|
||||
}
|
||||
|
||||
static importJSON(serializedTabNode: SerializedTabNode): TabNode {
|
||||
const node = $createTabNode();
|
||||
node.setFormat(serializedTabNode.format);
|
||||
node.setStyle(serializedTabNode.style);
|
||||
return node;
|
||||
return $createTabNode().updateFromJSON(serializedTabNode);
|
||||
}
|
||||
|
||||
setTextContent(_text: string): this {
|
||||
invariant(false, 'TabNode does not support setTextContent');
|
||||
setTextContent(text: string): this {
|
||||
invariant(
|
||||
text === '\t' || text === '',
|
||||
'TabNode does not support setTextContent',
|
||||
);
|
||||
return super.setTextContent(text);
|
||||
}
|
||||
|
||||
setDetail(_detail: TextDetailType | number): this {
|
||||
invariant(false, 'TabNode does not support setDetail');
|
||||
setDetail(detail: TextDetailType | number): this {
|
||||
invariant(detail === IS_UNMERGEABLE, 'TabNode does not support setDetail');
|
||||
return this;
|
||||
}
|
||||
|
||||
setMode(_type: TextModeType): this {
|
||||
invariant(false, 'TabNode does not support setMode');
|
||||
setMode(type: TextModeType): this {
|
||||
invariant(type !== 'normal', 'TabNode does not support setMode');
|
||||
return this;
|
||||
}
|
||||
|
||||
canInsertTextBefore(): boolean {
|
||||
|
@ -17,6 +17,7 @@ import type {
|
||||
DOMConversionMap,
|
||||
DOMConversionOutput,
|
||||
DOMExportOutput,
|
||||
LexicalUpdateJSON,
|
||||
NodeKey,
|
||||
SerializedLexicalNode,
|
||||
} from '../LexicalNode';
|
||||
@ -29,8 +30,6 @@ import invariant from 'shared/invariant';
|
||||
import {
|
||||
COMPOSITION_SUFFIX,
|
||||
DETAIL_TYPE_TO_DETAIL,
|
||||
DOM_ELEMENT_TYPE,
|
||||
DOM_TEXT_TYPE,
|
||||
IS_BOLD,
|
||||
IS_CODE,
|
||||
IS_DIRECTIONLESS,
|
||||
@ -62,6 +61,7 @@ import {
|
||||
$setCompositionKey,
|
||||
getCachedClassNameArray,
|
||||
internalMarkSiblingsAsDirty,
|
||||
isDOMTextNode,
|
||||
isHTMLElement,
|
||||
isInlineDomNode,
|
||||
toggleTextFormatType,
|
||||
@ -308,13 +308,14 @@ export class TextNode extends LexicalNode {
|
||||
|
||||
afterCloneFrom(prevNode: this): void {
|
||||
super.afterCloneFrom(prevNode);
|
||||
this.__text = prevNode.__text;
|
||||
this.__format = prevNode.__format;
|
||||
this.__style = prevNode.__style;
|
||||
this.__mode = prevNode.__mode;
|
||||
this.__detail = prevNode.__detail;
|
||||
}
|
||||
|
||||
constructor(text: string, key?: NodeKey) {
|
||||
constructor(text: string = '', key?: NodeKey) {
|
||||
super(key);
|
||||
this.__text = text;
|
||||
this.__format = 0;
|
||||
@ -606,12 +607,17 @@ 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;
|
||||
return $createTextNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
updateFromJSON(serializedNode: LexicalUpdateJSON<SerializedTextNode>): this {
|
||||
return super
|
||||
.updateFromJSON(serializedNode)
|
||||
.setTextContent(serializedNode.text)
|
||||
.setFormat(serializedNode.format)
|
||||
.setDetail(serializedNode.detail)
|
||||
.setMode(serializedNode.mode)
|
||||
.setStyle(serializedNode.style);
|
||||
}
|
||||
|
||||
// This improves Lexical's basic text output in copy+paste plus
|
||||
@ -1138,13 +1144,13 @@ function convertBringAttentionToElement(
|
||||
const preParentCache = new WeakMap<Node, null | Node>();
|
||||
|
||||
function isNodePre(node: Node): boolean {
|
||||
return (
|
||||
node.nodeName === 'PRE' ||
|
||||
(node.nodeType === DOM_ELEMENT_TYPE &&
|
||||
(node as HTMLElement).style !== undefined &&
|
||||
(node as HTMLElement).style.whiteSpace !== undefined &&
|
||||
(node as HTMLElement).style.whiteSpace.startsWith('pre'))
|
||||
);
|
||||
if (!isHTMLElement(node)) {
|
||||
return false;
|
||||
} else if (node.nodeName === 'PRE') {
|
||||
return true;
|
||||
}
|
||||
const whiteSpace = node.style.whiteSpace;
|
||||
return typeof whiteSpace === 'string' && whiteSpace.startsWith('pre');
|
||||
}
|
||||
|
||||
export function findParentPreDOMNode(node: Node) {
|
||||
@ -1260,8 +1266,8 @@ function findTextInLine(text: Text, forward: boolean): null | Text {
|
||||
node = parentElement;
|
||||
}
|
||||
node = sibling;
|
||||
if (node.nodeType === DOM_ELEMENT_TYPE) {
|
||||
const display = (node as HTMLElement).style.display;
|
||||
if (isHTMLElement(node)) {
|
||||
const display = node.style.display;
|
||||
if (
|
||||
(display === '' && !isInlineDomNode(node)) ||
|
||||
(display !== '' && !display.startsWith('inline'))
|
||||
@ -1273,8 +1279,8 @@ function findTextInLine(text: Text, forward: boolean): null | Text {
|
||||
while ((descendant = forward ? node.firstChild : node.lastChild) !== null) {
|
||||
node = descendant;
|
||||
}
|
||||
if (node.nodeType === DOM_TEXT_TYPE) {
|
||||
return node as Text;
|
||||
if (isDOMTextNode(node)) {
|
||||
return node;
|
||||
} else if (node.nodeName === 'BR') {
|
||||
return null;
|
||||
}
|
||||
|
Reference in New Issue
Block a user