lexical
Version:
Lexical is an extensible text editor framework that provides excellent reliability, accessible and performance.
198 lines (175 loc) • 5.2 kB
text/typescript
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {
EditorConfig,
KlassConstructor,
LexicalEditor,
Spread,
} from '../LexicalEditor';
import type {
DOMConversionMap,
DOMConversionOutput,
DOMExportOutput,
LexicalNode,
} from '../LexicalNode';
import type {RangeSelection} from '../LexicalSelection';
import type {
ElementFormatType,
SerializedElementNode,
} from './LexicalElementNode';
import {ELEMENT_TYPE_TO_FORMAT} from '../LexicalConstants';
import {
$applyNodeReplacement,
$setDirectionFromDOM,
$setFormatFromDOM,
getCachedClassNameArray,
isHTMLElement,
setNodeIndentFromDOM,
} from '../LexicalUtils';
import {ElementNode} from './LexicalElementNode';
import {$isTextNode} from './LexicalTextNode';
export type SerializedParagraphNode = Spread<
{
textFormat: number;
textStyle: string;
},
SerializedElementNode
>;
/** @noInheritDoc */
export class ParagraphNode extends ElementNode {
/** @internal */
declare ['constructor']: KlassConstructor<typeof ParagraphNode>;
static getType(): string {
return 'paragraph';
}
static clone(node: ParagraphNode): ParagraphNode {
return new ParagraphNode(node.__key);
}
// View
createDOM(config: EditorConfig): HTMLElement {
const dom = document.createElement('p');
const classNames = getCachedClassNameArray(config.theme, 'paragraph');
if (classNames !== undefined) {
const domClassList = dom.classList;
domClassList.add(...classNames);
}
return dom;
}
updateDOM(
prevNode: ParagraphNode,
dom: HTMLElement,
config: EditorConfig,
): boolean {
return false;
}
static importDOM(): DOMConversionMap | null {
return {
p: (node: Node) => ({
conversion: $convertParagraphElement,
priority: 0,
}),
};
}
exportDOM(editor: LexicalEditor): DOMExportOutput {
const {element} = super.exportDOM(editor);
if (isHTMLElement(element)) {
if (this.isEmpty()) {
element.append(document.createElement('br'));
}
const formatType = this.getFormatType();
if (formatType) {
element.style.textAlign = formatType;
}
}
return {
element,
};
}
static importJSON(serializedNode: SerializedParagraphNode): ParagraphNode {
return $createParagraphNode().updateFromJSON(serializedNode);
}
exportJSON(): SerializedParagraphNode {
const json = super.exportJSON();
// Provide backwards compatible values, see #7971
if (json.textFormat === undefined || json.textStyle === undefined) {
// Compute the same value that the reconciler would
const firstTextNode = this.getChildren().find($isTextNode);
if (firstTextNode) {
json.textFormat = firstTextNode.getFormat();
json.textStyle = firstTextNode.getStyle();
} else {
json.textFormat = this.getTextFormat();
json.textStyle = this.getTextStyle();
}
}
return json as SerializedParagraphNode;
}
// Mutation
insertNewAfter(
rangeSelection: RangeSelection,
restoreSelection: boolean,
): ParagraphNode {
const newElement = $createParagraphNode();
newElement.setTextFormat(rangeSelection.format);
newElement.setTextStyle(rangeSelection.style);
const direction = this.getDirection();
newElement.setDirection(direction);
newElement.setFormat(this.getFormatType());
newElement.setStyle(this.getStyle());
this.insertAfter(newElement, restoreSelection);
return newElement;
}
collapseAtStart(): boolean {
const children = this.getChildren();
// If we have an empty (trimmed) first paragraph and try and remove it,
// delete the paragraph as long as we have another sibling to go to
if (
children.length === 0 ||
($isTextNode(children[0]) && children[0].getTextContent().trim() === '')
) {
const nextSibling = this.getNextSibling();
if (nextSibling !== null) {
this.selectNext();
this.remove();
return true;
}
const prevSibling = this.getPreviousSibling();
if (prevSibling !== null) {
this.selectPrevious();
this.remove();
return true;
}
}
return false;
}
}
function $convertParagraphElement(element: HTMLElement): DOMConversionOutput {
const node = $createParagraphNode();
$setFormatFromDOM(node, element);
setNodeIndentFromDOM(element, node);
// Check legacy 'align' attribute
// Only use this if no format was set by CSS
if (node.getFormatType() === '') {
const align = element.getAttribute('align');
if (align) {
if (align && align in ELEMENT_TYPE_TO_FORMAT) {
node.setFormat(align as ElementFormatType);
}
}
}
$setDirectionFromDOM(node, element);
return {node};
}
export function $createParagraphNode(): ParagraphNode {
return $applyNodeReplacement(new ParagraphNode());
}
export function $isParagraphNode(
node: LexicalNode | null | undefined,
): node is ParagraphNode {
return node instanceof ParagraphNode;
}