Lexical: Merged custom paragraph node, removed old format/indent refs

Start of work to merge custom nodes into lexical, removing old unused
format/indent core logic while extending common block elements where
possible.
This commit is contained in:
Dan Brown 2024-12-03 16:24:49 +00:00
parent 5164375b18
commit f3fa63a5ae
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
22 changed files with 95 additions and 445 deletions

View File

@ -355,7 +355,6 @@ function onSelectionChange(
lastNode instanceof ParagraphNode && lastNode instanceof ParagraphNode &&
lastNode.getChildrenSize() === 0 lastNode.getChildrenSize() === 0
) { ) {
selection.format = lastNode.getTextFormat();
selection.style = lastNode.getTextStyle(); selection.style = lastNode.getTextStyle();
} else { } else {
selection.format = 0; selection.format = 0;
@ -578,7 +577,6 @@ function onBeforeInput(event: InputEvent, editor: LexicalEditor): void {
if ($isRangeSelection(selection)) { if ($isRangeSelection(selection)) {
const anchorNode = selection.anchor.getNode(); const anchorNode = selection.anchor.getNode();
anchorNode.markDirty(); anchorNode.markDirty();
selection.format = anchorNode.getFormat();
invariant( invariant(
$isTextNode(anchorNode), $isTextNode(anchorNode),
'Anchor node must be a TextNode', 'Anchor node must be a TextNode',
@ -912,7 +910,6 @@ function onCompositionStart(
// need to invoke the empty space heuristic below. // need to invoke the empty space heuristic below.
anchor.type === 'element' || anchor.type === 'element' ||
!selection.isCollapsed() || !selection.isCollapsed() ||
node.getFormat() !== selection.format ||
($isTextNode(node) && node.getStyle() !== selection.style) ($isTextNode(node) && node.getStyle() !== selection.style)
) { ) {
// We insert a zero width character, ready for the composition // We insert a zero width character, ready for the composition

View File

@ -96,15 +96,6 @@ function shouldUpdateTextNodeFromMutation(
targetDOM: Node, targetDOM: Node,
targetNode: TextNode, targetNode: TextNode,
): boolean { ): boolean {
if ($isRangeSelection(selection)) {
const anchorNode = selection.anchor.getNode();
if (
anchorNode.is(targetNode) &&
selection.format !== anchorNode.getFormat()
) {
return false;
}
}
return targetDOM.nodeType === DOM_TEXT_TYPE && targetNode.isAttached(); return targetDOM.nodeType === DOM_TEXT_TYPE && targetNode.isAttached();
} }

View File

@ -17,7 +17,6 @@ import type {NodeKey, NodeMap} from './LexicalNode';
import type {ElementNode} from './nodes/LexicalElementNode'; import type {ElementNode} from './nodes/LexicalElementNode';
import invariant from 'lexical/shared/invariant'; import invariant from 'lexical/shared/invariant';
import normalizeClassNames from 'lexical/shared/normalizeClassNames';
import { import {
$isDecoratorNode, $isDecoratorNode,
@ -117,51 +116,6 @@ function setTextAlign(domStyle: CSSStyleDeclaration, value: string): void {
domStyle.setProperty('text-align', value); domStyle.setProperty('text-align', value);
} }
const DEFAULT_INDENT_VALUE = '40px';
function setElementIndent(dom: HTMLElement, indent: number): void {
const indentClassName = activeEditorConfig.theme.indent;
if (typeof indentClassName === 'string') {
const elementHasClassName = dom.classList.contains(indentClassName);
if (indent > 0 && !elementHasClassName) {
dom.classList.add(indentClassName);
} else if (indent < 1 && elementHasClassName) {
dom.classList.remove(indentClassName);
}
}
const indentationBaseValue =
getComputedStyle(dom).getPropertyValue('--lexical-indent-base-value') ||
DEFAULT_INDENT_VALUE;
dom.style.setProperty(
'padding-inline-start',
indent === 0 ? '' : `calc(${indent} * ${indentationBaseValue})`,
);
}
function setElementFormat(dom: HTMLElement, format: number): void {
const domStyle = dom.style;
if (format === 0) {
setTextAlign(domStyle, '');
} else if (format === IS_ALIGN_LEFT) {
setTextAlign(domStyle, 'left');
} else if (format === IS_ALIGN_CENTER) {
setTextAlign(domStyle, 'center');
} else if (format === IS_ALIGN_RIGHT) {
setTextAlign(domStyle, 'right');
} else if (format === IS_ALIGN_JUSTIFY) {
setTextAlign(domStyle, 'justify');
} else if (format === IS_ALIGN_START) {
setTextAlign(domStyle, 'start');
} else if (format === IS_ALIGN_END) {
setTextAlign(domStyle, 'end');
}
}
function $createNode( function $createNode(
key: NodeKey, key: NodeKey,
parentDOM: null | HTMLElement, parentDOM: null | HTMLElement,
@ -185,22 +139,14 @@ function $createNode(
} }
if ($isElementNode(node)) { if ($isElementNode(node)) {
const indent = node.__indent;
const childrenSize = node.__size; const childrenSize = node.__size;
if (indent !== 0) {
setElementIndent(dom, indent);
}
if (childrenSize !== 0) { if (childrenSize !== 0) {
const endIndex = childrenSize - 1; const endIndex = childrenSize - 1;
const children = createChildrenArray(node, activeNextNodeMap); const children = createChildrenArray(node, activeNextNodeMap);
$createChildren(children, node, 0, endIndex, dom, null); $createChildren(children, node, 0, endIndex, dom, null);
} }
const format = node.__format;
if (format !== 0) {
setElementFormat(dom, format);
}
if (!node.isInline()) { if (!node.isInline()) {
reconcileElementTerminatingLineBreak(null, node, dom); reconcileElementTerminatingLineBreak(null, node, dom);
} }
@ -349,10 +295,8 @@ function reconcileParagraphFormat(element: ElementNode): void {
if ( if (
$isParagraphNode(element) && $isParagraphNode(element) &&
subTreeTextFormat != null && subTreeTextFormat != null &&
subTreeTextFormat !== element.__textFormat &&
!activeEditorStateReadOnly !activeEditorStateReadOnly
) { ) {
element.setTextFormat(subTreeTextFormat);
element.setTextStyle(subTreeTextStyle); element.setTextStyle(subTreeTextStyle);
} }
} }
@ -563,17 +507,6 @@ function $reconcileNode(
if ($isElementNode(prevNode) && $isElementNode(nextNode)) { if ($isElementNode(prevNode) && $isElementNode(nextNode)) {
// Reconcile element children // Reconcile element children
const nextIndent = nextNode.__indent;
if (nextIndent !== prevNode.__indent) {
setElementIndent(dom, nextIndent);
}
const nextFormat = nextNode.__format;
if (nextFormat !== prevNode.__format) {
setElementFormat(dom, nextFormat);
}
if (isDirty) { if (isDirty) {
$reconcileChildrenWithDirection(prevNode, nextNode, dom); $reconcileChildrenWithDirection(prevNode, nextNode, dom);
if (!$isRootNode(nextNode) && !nextNode.isInline()) { if (!$isRootNode(nextNode) && !nextNode.isInline()) {

View File

@ -129,8 +129,6 @@ export class TestElementNode extends ElementNode {
serializedNode: SerializedTestElementNode, serializedNode: SerializedTestElementNode,
): TestInlineElementNode { ): TestInlineElementNode {
const node = $createTestInlineElementNode(); const node = $createTestInlineElementNode();
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction); node.setDirection(serializedNode.direction);
return node; return node;
} }
@ -195,8 +193,6 @@ export class TestInlineElementNode extends ElementNode {
serializedNode: SerializedTestInlineElementNode, serializedNode: SerializedTestInlineElementNode,
): TestInlineElementNode { ): TestInlineElementNode {
const node = $createTestInlineElementNode(); const node = $createTestInlineElementNode();
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction); node.setDirection(serializedNode.direction);
return node; return node;
} }
@ -241,8 +237,6 @@ export class TestShadowRootNode extends ElementNode {
serializedNode: SerializedTestShadowRootNode, serializedNode: SerializedTestShadowRootNode,
): TestShadowRootNode { ): TestShadowRootNode {
const node = $createTestShadowRootNode(); const node = $createTestShadowRootNode();
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction); node.setDirection(serializedNode.direction);
return node; return node;
} }
@ -322,8 +316,6 @@ export class TestExcludeFromCopyElementNode extends ElementNode {
serializedNode: SerializedTestExcludeFromCopyElementNode, serializedNode: SerializedTestExcludeFromCopyElementNode,
): TestExcludeFromCopyElementNode { ): TestExcludeFromCopyElementNode {
const node = $createTestExcludeFromCopyElementNode(); const node = $createTestExcludeFromCopyElementNode();
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction); node.setDirection(serializedNode.direction);
return node; return node;
} }

View File

@ -0,0 +1,54 @@
import {ElementNode} from "./LexicalElementNode";
import {CommonBlockAlignment, SerializedCommonBlockNode} from "../../../nodes/_common";
export class CommonBlockNode extends ElementNode {
__id: string = '';
__alignment: CommonBlockAlignment = '';
__inset: number = 0;
setId(id: string) {
const self = this.getWritable();
self.__id = id;
}
getId(): string {
const self = this.getLatest();
return self.__id;
}
setAlignment(alignment: CommonBlockAlignment) {
const self = this.getWritable();
self.__alignment = alignment;
}
getAlignment(): CommonBlockAlignment {
const self = this.getLatest();
return self.__alignment;
}
setInset(size: number) {
const self = this.getWritable();
self.__inset = size;
}
getInset(): number {
const self = this.getLatest();
return self.__inset;
}
exportJSON(): SerializedCommonBlockNode {
return {
...super.exportJSON(),
id: this.__id,
alignment: this.__alignment,
inset: this.__inset,
};
}
}
export function copyCommonBlockProperties(from: CommonBlockNode, to: CommonBlockNode): void {
to.__id = from.__id;
to.__alignment = from.__alignment;
to.__inset = from.__inset;
}

View File

@ -42,8 +42,6 @@ export type SerializedElementNode<
{ {
children: Array<T>; children: Array<T>;
direction: 'ltr' | 'rtl' | null; direction: 'ltr' | 'rtl' | null;
format: ElementFormatType;
indent: number;
}, },
SerializedLexicalNode SerializedLexicalNode
>; >;
@ -74,12 +72,8 @@ export class ElementNode extends LexicalNode {
/** @internal */ /** @internal */
__size: number; __size: number;
/** @internal */ /** @internal */
__format: number;
/** @internal */
__style: string; __style: string;
/** @internal */ /** @internal */
__indent: number;
/** @internal */
__dir: 'ltr' | 'rtl' | null; __dir: 'ltr' | 'rtl' | null;
constructor(key?: NodeKey) { constructor(key?: NodeKey) {
@ -87,9 +81,7 @@ export class ElementNode extends LexicalNode {
this.__first = null; this.__first = null;
this.__last = null; this.__last = null;
this.__size = 0; this.__size = 0;
this.__format = 0;
this.__style = ''; this.__style = '';
this.__indent = 0;
this.__dir = null; this.__dir = null;
} }
@ -98,28 +90,14 @@ export class ElementNode extends LexicalNode {
this.__first = prevNode.__first; this.__first = prevNode.__first;
this.__last = prevNode.__last; this.__last = prevNode.__last;
this.__size = prevNode.__size; this.__size = prevNode.__size;
this.__indent = prevNode.__indent;
this.__format = prevNode.__format;
this.__style = prevNode.__style; this.__style = prevNode.__style;
this.__dir = prevNode.__dir; this.__dir = prevNode.__dir;
} }
getFormat(): number {
const self = this.getLatest();
return self.__format;
}
getFormatType(): ElementFormatType {
const format = this.getFormat();
return ELEMENT_FORMAT_TO_TYPE[format] || '';
}
getStyle(): string { getStyle(): string {
const self = this.getLatest(); const self = this.getLatest();
return self.__style; return self.__style;
} }
getIndent(): number {
const self = this.getLatest();
return self.__indent;
}
getChildren<T extends LexicalNode>(): Array<T> { getChildren<T extends LexicalNode>(): Array<T> {
const children: Array<T> = []; const children: Array<T> = [];
let child: T | null = this.getFirstChild(); let child: T | null = this.getFirstChild();
@ -301,13 +279,6 @@ export class ElementNode extends LexicalNode {
const self = this.getLatest(); const self = this.getLatest();
return self.__dir; return self.__dir;
} }
hasFormat(type: ElementFormatType): boolean {
if (type !== '') {
const formatFlag = ELEMENT_TYPE_TO_FORMAT[type];
return (this.getFormat() & formatFlag) !== 0;
}
return false;
}
// Mutators // Mutators
@ -378,21 +349,11 @@ export class ElementNode extends LexicalNode {
self.__dir = direction; self.__dir = direction;
return self; return self;
} }
setFormat(type: ElementFormatType): this {
const self = this.getWritable();
self.__format = type !== '' ? ELEMENT_TYPE_TO_FORMAT[type] : 0;
return this;
}
setStyle(style: string): this { setStyle(style: string): this {
const self = this.getWritable(); const self = this.getWritable();
self.__style = style || ''; self.__style = style || '';
return this; return this;
} }
setIndent(indentLevel: number): this {
const self = this.getWritable();
self.__indent = indentLevel;
return this;
}
splice( splice(
start: number, start: number,
deleteCount: number, deleteCount: number,
@ -528,8 +489,6 @@ export class ElementNode extends LexicalNode {
return { return {
children: [], children: [],
direction: this.getDirection(), direction: this.getDirection(),
format: this.getFormatType(),
indent: this.getIndent(),
type: 'element', type: 'element',
version: 1, version: 1,
}; };

View File

@ -19,39 +19,36 @@ import type {
LexicalNode, LexicalNode,
NodeKey, NodeKey,
} from '../LexicalNode'; } from '../LexicalNode';
import type {
ElementFormatType,
SerializedElementNode,
} from './LexicalElementNode';
import type {RangeSelection} from 'lexical'; import type {RangeSelection} from 'lexical';
import {TEXT_TYPE_TO_FORMAT} from '../LexicalConstants';
import { import {
$applyNodeReplacement, $applyNodeReplacement,
getCachedClassNameArray, getCachedClassNameArray,
isHTMLElement, isHTMLElement,
} from '../LexicalUtils'; } from '../LexicalUtils';
import {ElementNode} from './LexicalElementNode'; import {$isTextNode} from './LexicalTextNode';
import {$isTextNode, TextFormatType} from './LexicalTextNode'; import {
commonPropertiesDifferent, deserializeCommonBlockNode,
SerializedCommonBlockNode, setCommonBlockPropsFromElement,
updateElementWithCommonBlockProps
} from "../../../nodes/_common";
import {CommonBlockNode, copyCommonBlockProperties} from "lexical/nodes/CommonBlockNode";
export type SerializedParagraphNode = Spread< export type SerializedParagraphNode = Spread<
{ {
textFormat: number;
textStyle: string; textStyle: string;
}, },
SerializedElementNode SerializedCommonBlockNode
>; >;
/** @noInheritDoc */ /** @noInheritDoc */
export class ParagraphNode extends ElementNode { export class ParagraphNode extends CommonBlockNode {
['constructor']!: KlassConstructor<typeof ParagraphNode>; ['constructor']!: KlassConstructor<typeof ParagraphNode>;
/** @internal */ /** @internal */
__textFormat: number;
__textStyle: string; __textStyle: string;
constructor(key?: NodeKey) { constructor(key?: NodeKey) {
super(key); super(key);
this.__textFormat = 0;
this.__textStyle = ''; this.__textStyle = '';
} }
@ -59,22 +56,6 @@ export class ParagraphNode extends ElementNode {
return 'paragraph'; 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;
}
getTextStyle(): string { getTextStyle(): string {
const self = this.getLatest(); const self = this.getLatest();
return self.__textStyle; return self.__textStyle;
@ -92,8 +73,8 @@ export class ParagraphNode extends ElementNode {
afterCloneFrom(prevNode: this) { afterCloneFrom(prevNode: this) {
super.afterCloneFrom(prevNode); super.afterCloneFrom(prevNode);
this.__textFormat = prevNode.__textFormat;
this.__textStyle = prevNode.__textStyle; this.__textStyle = prevNode.__textStyle;
copyCommonBlockProperties(prevNode, this);
} }
// View // View
@ -105,6 +86,9 @@ export class ParagraphNode extends ElementNode {
const domClassList = dom.classList; const domClassList = dom.classList;
domClassList.add(...classNames); domClassList.add(...classNames);
} }
updateElementWithCommonBlockProps(dom, this);
return dom; return dom;
} }
updateDOM( updateDOM(
@ -112,7 +96,7 @@ export class ParagraphNode extends ElementNode {
dom: HTMLElement, dom: HTMLElement,
config: EditorConfig, config: EditorConfig,
): boolean { ): boolean {
return false; return commonPropertiesDifferent(prevNode, this);
} }
static importDOM(): DOMConversionMap | null { static importDOM(): DOMConversionMap | null {
@ -131,16 +115,6 @@ export class ParagraphNode extends ElementNode {
if (this.isEmpty()) { if (this.isEmpty()) {
element.append(document.createElement('br')); element.append(document.createElement('br'));
} }
const formatType = this.getFormatType();
element.style.textAlign = formatType;
const indent = this.getIndent();
if (indent > 0) {
// padding-inline-start is not widely supported in email HTML, but
// Lexical Reconciler uses padding-inline-start. Using text-indent instead.
element.style.textIndent = `${indent * 20}px`;
}
} }
return { return {
@ -150,16 +124,13 @@ export class ParagraphNode extends ElementNode {
static importJSON(serializedNode: SerializedParagraphNode): ParagraphNode { static importJSON(serializedNode: SerializedParagraphNode): ParagraphNode {
const node = $createParagraphNode(); const node = $createParagraphNode();
node.setFormat(serializedNode.format); deserializeCommonBlockNode(serializedNode, node);
node.setIndent(serializedNode.indent);
node.setTextFormat(serializedNode.textFormat);
return node; return node;
} }
exportJSON(): SerializedParagraphNode { exportJSON(): SerializedParagraphNode {
return { return {
...super.exportJSON(), ...super.exportJSON(),
textFormat: this.getTextFormat(),
textStyle: this.getTextStyle(), textStyle: this.getTextStyle(),
type: 'paragraph', type: 'paragraph',
version: 1, version: 1,
@ -173,11 +144,9 @@ export class ParagraphNode extends ElementNode {
restoreSelection: boolean, restoreSelection: boolean,
): ParagraphNode { ): ParagraphNode {
const newElement = $createParagraphNode(); const newElement = $createParagraphNode();
newElement.setTextFormat(rangeSelection.format);
newElement.setTextStyle(rangeSelection.style); newElement.setTextStyle(rangeSelection.style);
const direction = this.getDirection(); const direction = this.getDirection();
newElement.setDirection(direction); newElement.setDirection(direction);
newElement.setFormat(this.getFormatType());
newElement.setStyle(this.getTextStyle()); newElement.setStyle(this.getTextStyle());
this.insertAfter(newElement, restoreSelection); this.insertAfter(newElement, restoreSelection);
return newElement; return newElement;
@ -210,13 +179,7 @@ export class ParagraphNode extends ElementNode {
function $convertParagraphElement(element: HTMLElement): DOMConversionOutput { function $convertParagraphElement(element: HTMLElement): DOMConversionOutput {
const node = $createParagraphNode(); const node = $createParagraphNode();
if (element.style) { setCommonBlockPropsFromElement(element, node);
node.setFormat(element.style.textAlign as ElementFormatType);
const indent = parseInt(element.style.textIndent, 10) / 20;
if (indent > 0) {
node.setIndent(indent);
}
}
return {node}; return {node};
} }

View File

@ -99,8 +99,6 @@ export class RootNode extends ElementNode {
static importJSON(serializedNode: SerializedRootNode): RootNode { static importJSON(serializedNode: SerializedRootNode): RootNode {
// We don't create a root, and instead use the existing root. // We don't create a root, and instead use the existing root.
const node = $getRoot(); const node = $getRoot();
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction); node.setDirection(serializedNode.direction);
return node; return node;
} }
@ -109,8 +107,6 @@ export class RootNode extends ElementNode {
return { return {
children: [], children: [],
direction: this.getDirection(), direction: this.getDirection(),
format: this.getFormatType(),
indent: this.getIndent(),
type: 'root', type: 'root',
version: 1, version: 1,
}; };

View File

@ -327,9 +327,6 @@ function wrapContinuousInlines(
for (let i = 0; i < nodes.length; i++) { for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]; const node = nodes[i];
if ($isBlockElementNode(node)) { if ($isBlockElementNode(node)) {
if (textAlign && !node.getFormat()) {
node.setFormat(textAlign);
}
out.push(node); out.push(node);
} else { } else {
continuousInlines.push(node); continuousInlines.push(node);
@ -338,7 +335,6 @@ function wrapContinuousInlines(
(i < nodes.length - 1 && $isBlockElementNode(nodes[i + 1])) (i < nodes.length - 1 && $isBlockElementNode(nodes[i + 1]))
) { ) {
const wrapper = createWrapperFn(); const wrapper = createWrapperFn();
wrapper.setFormat(textAlign);
wrapper.append(...continuousInlines); wrapper.append(...continuousInlines);
out.push(wrapper); out.push(wrapper);
continuousInlines = []; continuousInlines = [];

View File

@ -162,8 +162,6 @@ export class LinkNode extends ElementNode {
target: serializedNode.target, target: serializedNode.target,
title: serializedNode.title, title: serializedNode.title,
}); });
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction); node.setDirection(serializedNode.direction);
return node; return node;
} }
@ -402,8 +400,6 @@ export class AutoLinkNode extends LinkNode {
target: serializedNode.target, target: serializedNode.target,
title: serializedNode.title, title: serializedNode.title,
}); });
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction); node.setDirection(serializedNode.direction);
return node; return node;
} }

View File

@ -126,14 +126,12 @@ export class ListItemNode extends ElementNode {
const node = $createListItemNode(); const node = $createListItemNode();
node.setChecked(serializedNode.checked); node.setChecked(serializedNode.checked);
node.setValue(serializedNode.value); node.setValue(serializedNode.value);
node.setFormat(serializedNode.format);
node.setDirection(serializedNode.direction); node.setDirection(serializedNode.direction);
return node; return node;
} }
exportDOM(editor: LexicalEditor): DOMExportOutput { exportDOM(editor: LexicalEditor): DOMExportOutput {
const element = this.createDOM(editor._config); const element = this.createDOM(editor._config);
element.style.textAlign = this.getFormatType();
return { return {
element, element,
}; };
@ -172,7 +170,6 @@ export class ListItemNode extends ElementNode {
if ($isListItemNode(replaceWithNode)) { if ($isListItemNode(replaceWithNode)) {
return super.replace(replaceWithNode); return super.replace(replaceWithNode);
} }
this.setIndent(0);
const list = this.getParentOrThrow(); const list = this.getParentOrThrow();
if (!$isListNode(list)) { if (!$isListNode(list)) {
return replaceWithNode; return replaceWithNode;
@ -351,41 +348,6 @@ export class ListItemNode extends ElementNode {
this.setChecked(!this.__checked); this.setChecked(!this.__checked);
} }
getIndent(): number {
// If we don't have a parent, we are likely serializing
const parent = this.getParent();
if (parent === null) {
return this.getLatest().__indent;
}
// ListItemNode should always have a ListNode for a parent.
let listNodeParent = parent.getParentOrThrow();
let indentLevel = 0;
while ($isListItemNode(listNodeParent)) {
listNodeParent = listNodeParent.getParentOrThrow().getParentOrThrow();
indentLevel++;
}
return indentLevel;
}
setIndent(indent: number): this {
invariant(typeof indent === 'number', 'Invalid indent value.');
indent = Math.floor(indent);
invariant(indent >= 0, 'Indent value must be non-negative.');
let currentIndent = this.getIndent();
while (currentIndent !== indent) {
if (currentIndent < indent) {
$handleIndent(this);
currentIndent++;
} else {
$handleOutdent(this);
currentIndent--;
}
}
return this;
}
/** @deprecated @internal */ /** @deprecated @internal */
canInsertAfter(node: LexicalNode): boolean { canInsertAfter(node: LexicalNode): boolean {
return $isListItemNode(node); return $isListItemNode(node);

View File

@ -84,10 +84,6 @@ export function insertList(editor: LexicalEditor, listType: ListType): void {
if ($isRootOrShadowRoot(anchorNodeParent)) { if ($isRootOrShadowRoot(anchorNodeParent)) {
anchorNode.replace(list); anchorNode.replace(list);
const listItem = $createListItemNode(); const listItem = $createListItemNode();
if ($isElementNode(anchorNode)) {
listItem.setFormat(anchorNode.getFormatType());
listItem.setIndent(anchorNode.getIndent());
}
list.append(listItem); list.append(listItem);
} else if ($isListItemNode(anchorNode)) { } else if ($isListItemNode(anchorNode)) {
const parent = anchorNode.getParentOrThrow(); const parent = anchorNode.getParentOrThrow();
@ -157,8 +153,6 @@ function $createListOrMerge(node: ElementNode, listType: ListType): ListNode {
const previousSibling = node.getPreviousSibling(); const previousSibling = node.getPreviousSibling();
const nextSibling = node.getNextSibling(); const nextSibling = node.getNextSibling();
const listItem = $createListItemNode(); const listItem = $createListItemNode();
listItem.setFormat(node.getFormatType());
listItem.setIndent(node.getIndent());
append(listItem, node.getChildren()); append(listItem, node.getChildren());
if ( if (

View File

@ -155,9 +155,6 @@ export class QuoteNode extends ElementNode {
if (this.isEmpty()) { if (this.isEmpty()) {
element.append(document.createElement('br')); element.append(document.createElement('br'));
} }
const formatType = this.getFormatType();
element.style.textAlign = formatType;
} }
return { return {
@ -167,8 +164,6 @@ export class QuoteNode extends ElementNode {
static importJSON(serializedNode: SerializedQuoteNode): QuoteNode { static importJSON(serializedNode: SerializedQuoteNode): QuoteNode {
const node = $createQuoteNode(); const node = $createQuoteNode();
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
return node; return node;
} }
@ -315,9 +310,6 @@ export class HeadingNode extends ElementNode {
if (this.isEmpty()) { if (this.isEmpty()) {
element.append(document.createElement('br')); element.append(document.createElement('br'));
} }
const formatType = this.getFormatType();
element.style.textAlign = formatType;
} }
return { return {
@ -326,10 +318,7 @@ export class HeadingNode extends ElementNode {
} }
static importJSON(serializedNode: SerializedHeadingNode): HeadingNode { static importJSON(serializedNode: SerializedHeadingNode): HeadingNode {
const node = $createHeadingNode(serializedNode.tag); return $createHeadingNode(serializedNode.tag);
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
return node;
} }
exportJSON(): SerializedHeadingNode { exportJSON(): SerializedHeadingNode {
@ -402,18 +391,12 @@ function $convertHeadingElement(element: HTMLElement): DOMConversionOutput {
nodeName === 'h6' nodeName === 'h6'
) { ) {
node = $createHeadingNode(nodeName); node = $createHeadingNode(nodeName);
if (element.style !== null) {
node.setFormat(element.style.textAlign as ElementFormatType);
}
} }
return {node}; return {node};
} }
function $convertBlockquoteElement(element: HTMLElement): DOMConversionOutput { function $convertBlockquoteElement(element: HTMLElement): DOMConversionOutput {
const node = $createQuoteNode(); const node = $createQuoteNode();
if (element.style !== null) {
node.setFormat(element.style.textAlign as ElementFormatType);
}
return {node}; return {node};
} }
@ -651,9 +634,6 @@ export function registerRichText(editor: LexicalEditor): () => void {
(parentNode): parentNode is ElementNode => (parentNode): parentNode is ElementNode =>
$isElementNode(parentNode) && !parentNode.isInline(), $isElementNode(parentNode) && !parentNode.isInline(),
); );
if (element !== null) {
element.setFormat(format);
}
} }
return true; return true;
}, },
@ -691,28 +671,6 @@ export function registerRichText(editor: LexicalEditor): () => void {
}, },
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR,
), ),
editor.registerCommand(
INDENT_CONTENT_COMMAND,
() => {
return $handleIndentAndOutdent((block) => {
const indent = block.getIndent();
block.setIndent(indent + 1);
});
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
OUTDENT_CONTENT_COMMAND,
() => {
return $handleIndentAndOutdent((block) => {
const indent = block.getIndent();
if (indent > 0) {
block.setIndent(indent - 1);
}
});
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand<KeyboardEvent>( editor.registerCommand<KeyboardEvent>(
KEY_ARROW_UP_COMMAND, KEY_ARROW_UP_COMMAND,
(event) => { (event) => {
@ -846,19 +804,7 @@ export function registerRichText(editor: LexicalEditor): () => void {
return false; return false;
} }
event.preventDefault(); event.preventDefault();
const {anchor} = selection;
const anchorNode = anchor.getNode();
if (
selection.isCollapsed() &&
anchor.offset === 0 &&
!$isRootNode(anchorNode)
) {
const element = $getNearestBlockElementAncestorOrThrow(anchorNode);
if (element.getIndent() > 0) {
return editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined);
}
}
return editor.dispatchCommand(DELETE_CHARACTER_COMMAND, true); return editor.dispatchCommand(DELETE_CHARACTER_COMMAND, true);
}, },
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR,

View File

@ -81,8 +81,6 @@ export function $setBlocksType(
invariant($isElementNode(node), 'Expected block node to be an ElementNode'); invariant($isElementNode(node), 'Expected block node to be an ElementNode');
const targetElement = createElement(); const targetElement = createElement();
targetElement.setFormat(node.getFormatType());
targetElement.setIndent(node.getIndent());
node.replace(targetElement, true); node.replace(targetElement, true);
} }
} }
@ -136,8 +134,6 @@ export function $wrapNodes(
: anchor.getNode(); : anchor.getNode();
const children = target.getChildren(); const children = target.getChildren();
let element = createElement(); let element = createElement();
element.setFormat(target.getFormatType());
element.setIndent(target.getIndent());
children.forEach((child) => element.append(child)); children.forEach((child) => element.append(child));
if (wrappingElement) { if (wrappingElement) {
@ -277,8 +273,6 @@ export function $wrapNodesImpl(
if (elementMapping.get(parentKey) === undefined) { if (elementMapping.get(parentKey) === undefined) {
const targetElement = createElement(); const targetElement = createElement();
targetElement.setFormat(parent.getFormatType());
targetElement.setIndent(parent.getIndent());
elements.push(targetElement); elements.push(targetElement);
elementMapping.set(parentKey, targetElement); elementMapping.set(parentKey, targetElement);
// Move node and its siblings to the new // Move node and its siblings to the new
@ -299,8 +293,6 @@ export function $wrapNodesImpl(
'Expected node in emptyElements to be an ElementNode', 'Expected node in emptyElements to be an ElementNode',
); );
const targetElement = createElement(); const targetElement = createElement();
targetElement.setFormat(node.getFormatType());
targetElement.setIndent(node.getIndent());
elements.push(targetElement); elements.push(targetElement);
node.remove(true); node.remove(true);
} }

View File

@ -1,123 +0,0 @@
import {
DOMConversion,
DOMConversionMap,
DOMConversionOutput,
LexicalNode,
ParagraphNode, SerializedParagraphNode, Spread,
} from "lexical";
import {EditorConfig} from "lexical/LexicalEditor";
import {
CommonBlockAlignment, commonPropertiesDifferent, deserializeCommonBlockNode,
SerializedCommonBlockNode,
setCommonBlockPropsFromElement,
updateElementWithCommonBlockProps
} from "./_common";
export type SerializedCustomParagraphNode = Spread<SerializedCommonBlockNode, SerializedParagraphNode>
export class CustomParagraphNode extends ParagraphNode {
__id: string = '';
__alignment: CommonBlockAlignment = '';
__inset: number = 0;
static getType() {
return 'custom-paragraph';
}
setId(id: string) {
const self = this.getWritable();
self.__id = id;
}
getId(): string {
const self = this.getLatest();
return self.__id;
}
setAlignment(alignment: CommonBlockAlignment) {
const self = this.getWritable();
self.__alignment = alignment;
}
getAlignment(): CommonBlockAlignment {
const self = this.getLatest();
return self.__alignment;
}
setInset(size: number) {
const self = this.getWritable();
self.__inset = size;
}
getInset(): number {
const self = this.getLatest();
return self.__inset;
}
static clone(node: CustomParagraphNode): CustomParagraphNode {
const newNode = new CustomParagraphNode(node.__key);
newNode.__id = node.__id;
newNode.__alignment = node.__alignment;
newNode.__inset = node.__inset;
return newNode;
}
createDOM(config: EditorConfig): HTMLElement {
const dom = super.createDOM(config);
updateElementWithCommonBlockProps(dom, this);
return dom;
}
updateDOM(prevNode: CustomParagraphNode, dom: HTMLElement, config: EditorConfig): boolean {
return super.updateDOM(prevNode, dom, config)
|| commonPropertiesDifferent(prevNode, this);
}
exportJSON(): SerializedCustomParagraphNode {
return {
...super.exportJSON(),
type: 'custom-paragraph',
version: 1,
id: this.__id,
alignment: this.__alignment,
inset: this.__inset,
};
}
static importJSON(serializedNode: SerializedCustomParagraphNode): CustomParagraphNode {
const node = $createCustomParagraphNode();
deserializeCommonBlockNode(serializedNode, node);
return node;
}
static importDOM(): DOMConversionMap|null {
return {
p(node: HTMLElement): DOMConversion|null {
return {
conversion: (element: HTMLElement): DOMConversionOutput|null => {
const node = $createCustomParagraphNode();
if (element.style.textIndent) {
const indent = parseInt(element.style.textIndent, 10) / 20;
if (indent > 0) {
node.setIndent(indent);
}
}
setCommonBlockPropsFromElement(element, node);
return {node};
},
priority: 1,
};
},
};
}
}
export function $createCustomParagraphNode(): CustomParagraphNode {
return new CustomParagraphNode();
}
export function $isCustomParagraphNode(node: LexicalNode | null | undefined): node is CustomParagraphNode {
return node instanceof CustomParagraphNode;
}

View File

@ -7,7 +7,6 @@ import {
LexicalNodeReplacement, NodeMutation, LexicalNodeReplacement, NodeMutation,
ParagraphNode ParagraphNode
} from "lexical"; } from "lexical";
import {CustomParagraphNode} from "./custom-paragraph";
import {LinkNode} from "@lexical/link"; import {LinkNode} from "@lexical/link";
import {ImageNode} from "./image"; import {ImageNode} from "./image";
import {DetailsNode, SummaryNode} from "./details"; import {DetailsNode, SummaryNode} from "./details";
@ -45,14 +44,8 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
CodeBlockNode, CodeBlockNode,
DiagramNode, DiagramNode,
MediaNode, // TODO - Alignment MediaNode, // TODO - Alignment
CustomParagraphNode, ParagraphNode,
LinkNode, LinkNode,
{
replace: ParagraphNode,
with: (node: ParagraphNode) => {
return new CustomParagraphNode();
}
},
{ {
replace: HeadingNode, replace: HeadingNode,
with: (node: HeadingNode) => { with: (node: HeadingNode) => {

View File

@ -1,4 +1,5 @@
import { import {
$createParagraphNode,
$insertNodes, $insertNodes,
$isDecoratorNode, COMMAND_PRIORITY_HIGH, DROP_COMMAND, $isDecoratorNode, COMMAND_PRIORITY_HIGH, DROP_COMMAND,
LexicalEditor, LexicalEditor,
@ -8,7 +9,6 @@ import {$insertNewBlockNodesAtSelection, $selectSingleNode} from "../utils/selec
import {$getNearestBlockNodeForCoords, $htmlToBlockNodes} from "../utils/nodes"; import {$getNearestBlockNodeForCoords, $htmlToBlockNodes} from "../utils/nodes";
import {Clipboard} from "../../services/clipboard"; import {Clipboard} from "../../services/clipboard";
import {$createImageNode} from "../nodes/image"; import {$createImageNode} from "../nodes/image";
import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
import {$createLinkNode} from "@lexical/link"; import {$createLinkNode} from "@lexical/link";
import {EditorImageData, uploadImageFile} from "../utils/images"; import {EditorImageData, uploadImageFile} from "../utils/images";
import {EditorUiContext} from "../ui/framework/core"; import {EditorUiContext} from "../ui/framework/core";
@ -67,7 +67,7 @@ function handleMediaInsert(data: DataTransfer, context: EditorUiContext): boolea
for (const imageFile of images) { for (const imageFile of images) {
const loadingImage = window.baseUrl('/loading.gif'); const loadingImage = window.baseUrl('/loading.gif');
const loadingNode = $createImageNode(loadingImage); const loadingNode = $createImageNode(loadingImage);
const imageWrap = $createCustomParagraphNode(); const imageWrap = $createParagraphNode();
imageWrap.append(loadingNode); imageWrap.append(loadingNode);
$insertNodes([imageWrap]); $insertNodes([imageWrap]);

View File

@ -1,5 +1,6 @@
import {EditorUiContext} from "../ui/framework/core"; import {EditorUiContext} from "../ui/framework/core";
import { import {
$createParagraphNode,
$getSelection, $getSelection,
$isDecoratorNode, $isDecoratorNode,
COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_LOW,
@ -13,7 +14,6 @@ import {$isImageNode} from "../nodes/image";
import {$isMediaNode} from "../nodes/media"; import {$isMediaNode} from "../nodes/media";
import {getLastSelection} from "../utils/selection"; import {getLastSelection} from "../utils/selection";
import {$getNearestNodeBlockParent} from "../utils/nodes"; import {$getNearestNodeBlockParent} from "../utils/nodes";
import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
import {$isCustomListItemNode} from "../nodes/custom-list-item"; import {$isCustomListItemNode} from "../nodes/custom-list-item";
import {$setInsetForSelection} from "../utils/lists"; import {$setInsetForSelection} from "../utils/lists";
@ -45,7 +45,7 @@ function insertAfterSingleSelectedNode(editor: LexicalEditor, event: KeyboardEve
if (nearestBlock) { if (nearestBlock) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
editor.update(() => { editor.update(() => {
const newParagraph = $createCustomParagraphNode(); const newParagraph = $createParagraphNode();
nearestBlock.insertAfter(newParagraph); nearestBlock.insertAfter(newParagraph);
newParagraph.select(); newParagraph.select();
}); });

View File

@ -2,7 +2,11 @@
## In progress ## In progress
// Reorg
- Merge custom nodes into original nodes
- Reduce down to use CommonBlockNode where possible
- Remove existing formatType/ElementFormatType references (replaced with alignment).
- Remove existing indent references (replaced with inset).
## Main Todo ## Main Todo

View File

@ -1,5 +1,13 @@
import {$isQuoteNode, HeadingNode, HeadingTagType} from "@lexical/rich-text"; import {$isQuoteNode, HeadingNode, HeadingTagType} from "@lexical/rich-text";
import {$createTextNode, $getSelection, $insertNodes, LexicalEditor, LexicalNode} from "lexical"; import {
$createParagraphNode,
$createTextNode,
$getSelection,
$insertNodes,
$isParagraphNode,
LexicalEditor,
LexicalNode
} from "lexical";
import { import {
$getBlockElementNodesInSelection, $getBlockElementNodesInSelection,
$getNodeFromSelection, $getNodeFromSelection,
@ -8,7 +16,6 @@ import {
getLastSelection getLastSelection
} from "./selection"; } from "./selection";
import {$createCustomHeadingNode, $isCustomHeadingNode} from "../nodes/custom-heading"; import {$createCustomHeadingNode, $isCustomHeadingNode} from "../nodes/custom-heading";
import {$createCustomParagraphNode, $isCustomParagraphNode} from "../nodes/custom-paragraph";
import {$createCustomQuoteNode} from "../nodes/custom-quote"; import {$createCustomQuoteNode} from "../nodes/custom-quote";
import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../nodes/code-block"; import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../nodes/code-block";
import {$createCalloutNode, $isCalloutNode, CalloutCategory} from "../nodes/callout"; import {$createCalloutNode, $isCalloutNode, CalloutCategory} from "../nodes/callout";
@ -31,7 +38,7 @@ export function toggleSelectionAsHeading(editor: LexicalEditor, tag: HeadingTagT
export function toggleSelectionAsParagraph(editor: LexicalEditor) { export function toggleSelectionAsParagraph(editor: LexicalEditor) {
editor.update(() => { editor.update(() => {
$toggleSelectionBlockNodeType($isCustomParagraphNode, $createCustomParagraphNode); $toggleSelectionBlockNodeType($isParagraphNode, $createParagraphNode);
}); });
} }

View File

@ -1,4 +1,5 @@
import { import {
$createParagraphNode,
$getRoot, $getRoot,
$isDecoratorNode, $isDecoratorNode,
$isElementNode, $isRootNode, $isElementNode, $isRootNode,
@ -8,7 +9,6 @@ import {
LexicalNode LexicalNode
} from "lexical"; } from "lexical";
import {LexicalNodeMatcher} from "../nodes"; import {LexicalNodeMatcher} from "../nodes";
import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
import {$generateNodesFromDOM} from "@lexical/html"; import {$generateNodesFromDOM} from "@lexical/html";
import {htmlToDom} from "./dom"; import {htmlToDom} from "./dom";
import {NodeHasAlignment, NodeHasInset} from "../nodes/_common"; import {NodeHasAlignment, NodeHasInset} from "../nodes/_common";
@ -17,7 +17,7 @@ import {$findMatchingParent} from "@lexical/utils";
function wrapTextNodes(nodes: LexicalNode[]): LexicalNode[] { function wrapTextNodes(nodes: LexicalNode[]): LexicalNode[] {
return nodes.map(node => { return nodes.map(node => {
if ($isTextNode(node)) { if ($isTextNode(node)) {
const paragraph = $createCustomParagraphNode(); const paragraph = $createParagraphNode();
paragraph.append(node); paragraph.append(node);
return paragraph; return paragraph;
} }

View File

@ -7,17 +7,15 @@ import {
$isTextNode, $isTextNode,
$setSelection, $setSelection,
BaseSelection, DecoratorNode, BaseSelection, DecoratorNode,
ElementFormatType,
ElementNode, LexicalEditor, ElementNode, LexicalEditor,
LexicalNode, LexicalNode,
TextFormatType, TextNode TextFormatType, TextNode
} from "lexical"; } from "lexical";
import {$findMatchingParent, $getNearestBlockElementAncestorOrThrow} from "@lexical/utils"; import {$getNearestBlockElementAncestorOrThrow} from "@lexical/utils";
import {LexicalElementNodeCreator, LexicalNodeMatcher} from "../nodes"; import {LexicalElementNodeCreator, LexicalNodeMatcher} from "../nodes";
import {$setBlocksType} from "@lexical/selection"; import {$setBlocksType} from "@lexical/selection";
import {$getNearestNodeBlockParent, $getParentOfType, nodeHasAlignment} from "./nodes"; import {$getNearestNodeBlockParent, $getParentOfType, nodeHasAlignment} from "./nodes";
import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
import {CommonBlockAlignment} from "../nodes/_common"; import {CommonBlockAlignment} from "../nodes/_common";
const lastSelectionByEditor = new WeakMap<LexicalEditor, BaseSelection|null>; const lastSelectionByEditor = new WeakMap<LexicalEditor, BaseSelection|null>;
@ -71,7 +69,7 @@ export function $toggleSelectionBlockNodeType(matcher: LexicalNodeMatcher, creat
const selection = $getSelection(); const selection = $getSelection();
const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null; const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
if (selection && matcher(blockElement)) { if (selection && matcher(blockElement)) {
$setBlocksType(selection, $createCustomParagraphNode); $setBlocksType(selection, $createParagraphNode);
} else { } else {
$setBlocksType(selection, creator); $setBlocksType(selection, creator);
} }