14 type SerializedElementNode,
17 import {addClassNamesToElement} from "@lexical/utils";
18 import {CommonBlockNode, copyCommonBlockProperties} from "lexical/nodes/CommonBlockNode";
20 commonPropertiesDifferent, deserializeCommonBlockNode,
21 SerializedCommonBlockNode, setCommonBlockPropsFromElement,
22 updateElementWithCommonBlockProps
23 } from "../../nodes/_common";
25 export type HeadingTagType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
27 export type SerializedHeadingNode = Spread<
29 tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
31 SerializedCommonBlockNode
35 export class HeadingNode extends CommonBlockNode {
37 __tag: HeadingTagType;
39 static getType(): string {
43 static clone(node: HeadingNode): HeadingNode {
44 const clone = new HeadingNode(node.__tag, node.__key);
45 copyCommonBlockProperties(node, clone);
49 constructor(tag: HeadingTagType, key?: NodeKey) {
54 getTag(): HeadingTagType {
60 createDOM(config: EditorConfig): HTMLElement {
61 const tag = this.__tag;
62 const element = document.createElement(tag);
63 const theme = config.theme;
64 const classNames = theme.heading;
65 if (classNames !== undefined) {
66 const className = classNames[tag];
67 addClassNamesToElement(element, className);
69 updateElementWithCommonBlockProps(element, this);
73 updateDOM(prevNode: HeadingNode, dom: HTMLElement): boolean {
74 return commonPropertiesDifferent(prevNode, this);
77 static importDOM(): DOMConversionMap | null {
79 h1: (node: Node) => ({
80 conversion: $convertHeadingElement,
83 h2: (node: Node) => ({
84 conversion: $convertHeadingElement,
87 h3: (node: Node) => ({
88 conversion: $convertHeadingElement,
91 h4: (node: Node) => ({
92 conversion: $convertHeadingElement,
95 h5: (node: Node) => ({
96 conversion: $convertHeadingElement,
99 h6: (node: Node) => ({
100 conversion: $convertHeadingElement,
106 exportDOM(editor: LexicalEditor): DOMExportOutput {
107 const {element} = super.exportDOM(editor);
109 if (element && isHTMLElement(element)) {
110 if (this.isEmpty()) {
111 element.append(document.createElement('br'));
120 static importJSON(serializedNode: SerializedHeadingNode): HeadingNode {
121 const node = $createHeadingNode(serializedNode.tag);
122 deserializeCommonBlockNode(serializedNode, node);
126 exportJSON(): SerializedHeadingNode {
128 ...super.exportJSON(),
137 selection?: RangeSelection,
138 restoreSelection = true,
139 ): ParagraphNode | HeadingNode {
140 const anchorOffet = selection ? selection.anchor.offset : 0;
141 const lastDesc = this.getLastDescendant();
145 selection.anchor.key === lastDesc.getKey() &&
146 anchorOffet === lastDesc.getTextContentSize());
148 isAtEnd || !selection
149 ? $createParagraphNode()
150 : $createHeadingNode(this.getTag());
151 const direction = this.getDirection();
152 newElement.setDirection(direction);
153 this.insertAfter(newElement, restoreSelection);
154 if (anchorOffet === 0 && !this.isEmpty() && selection) {
155 const paragraph = $createParagraphNode();
157 this.replace(paragraph, true);
162 collapseAtStart(): true {
163 const newElement = !this.isEmpty()
164 ? $createHeadingNode(this.getTag())
165 : $createParagraphNode();
166 const children = this.getChildren();
167 children.forEach((child) => newElement.append(child));
168 this.replace(newElement);
172 extractWithChild(): boolean {
177 function $convertHeadingElement(element: HTMLElement): DOMConversionOutput {
178 const nodeName = element.nodeName.toLowerCase();
188 node = $createHeadingNode(nodeName);
189 setCommonBlockPropsFromElement(element, node);
194 export function $createHeadingNode(headingTag: HeadingTagType): HeadingNode {
195 return $applyNodeReplacement(new HeadingNode(headingTag));
198 export function $isHeadingNode(
199 node: LexicalNode | null | undefined,
200 ): node is HeadingNode {
201 return node instanceof HeadingNode;