1
+ import {
2
+ $applyNodeReplacement ,
3
+ $createParagraphNode ,
4
+ type DOMConversionMap ,
5
+ DOMConversionOutput ,
6
+ type DOMExportOutput ,
7
+ type EditorConfig ,
8
+ isHTMLElement ,
9
+ type LexicalEditor ,
10
+ type LexicalNode ,
11
+ type NodeKey ,
12
+ type ParagraphNode ,
13
+ type RangeSelection ,
14
+ type SerializedElementNode ,
15
+ type Spread
16
+ } from "lexical" ;
17
+ import { addClassNamesToElement } from "@lexical/utils" ;
18
+ import { CommonBlockNode , copyCommonBlockProperties } from "lexical/nodes/CommonBlockNode" ;
19
+ import {
20
+ commonPropertiesDifferent , deserializeCommonBlockNode ,
21
+ SerializedCommonBlockNode , setCommonBlockPropsFromElement ,
22
+ updateElementWithCommonBlockProps
23
+ } from "../../nodes/_common" ;
24
+
25
+ export type HeadingTagType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' ;
26
+
27
+ export type SerializedHeadingNode = Spread <
28
+ {
29
+ tag : 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' ;
30
+ } ,
31
+ SerializedCommonBlockNode
32
+ > ;
33
+
34
+ /** @noInheritDoc */
35
+ export class HeadingNode extends CommonBlockNode {
36
+ /** @internal */
37
+ __tag : HeadingTagType ;
38
+
39
+ static getType ( ) : string {
40
+ return 'heading' ;
41
+ }
42
+
43
+ static clone ( node : HeadingNode ) : HeadingNode {
44
+ const clone = new HeadingNode ( node . __tag , node . __key ) ;
45
+ copyCommonBlockProperties ( node , clone ) ;
46
+ return clone ;
47
+ }
48
+
49
+ constructor ( tag : HeadingTagType , key ?: NodeKey ) {
50
+ super ( key ) ;
51
+ this . __tag = tag ;
52
+ }
53
+
54
+ getTag ( ) : HeadingTagType {
55
+ return this . __tag ;
56
+ }
57
+
58
+ // View
59
+
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 ) ;
68
+ }
69
+ updateElementWithCommonBlockProps ( element , this ) ;
70
+ return element ;
71
+ }
72
+
73
+ updateDOM ( prevNode : HeadingNode , dom : HTMLElement ) : boolean {
74
+ return commonPropertiesDifferent ( prevNode , this ) ;
75
+ }
76
+
77
+ static importDOM ( ) : DOMConversionMap | null {
78
+ return {
79
+ h1 : ( node : Node ) => ( {
80
+ conversion : $convertHeadingElement ,
81
+ priority : 0 ,
82
+ } ) ,
83
+ h2 : ( node : Node ) => ( {
84
+ conversion : $convertHeadingElement ,
85
+ priority : 0 ,
86
+ } ) ,
87
+ h3 : ( node : Node ) => ( {
88
+ conversion : $convertHeadingElement ,
89
+ priority : 0 ,
90
+ } ) ,
91
+ h4 : ( node : Node ) => ( {
92
+ conversion : $convertHeadingElement ,
93
+ priority : 0 ,
94
+ } ) ,
95
+ h5 : ( node : Node ) => ( {
96
+ conversion : $convertHeadingElement ,
97
+ priority : 0 ,
98
+ } ) ,
99
+ h6 : ( node : Node ) => ( {
100
+ conversion : $convertHeadingElement ,
101
+ priority : 0 ,
102
+ } ) ,
103
+ } ;
104
+ }
105
+
106
+ exportDOM ( editor : LexicalEditor ) : DOMExportOutput {
107
+ const { element} = super . exportDOM ( editor ) ;
108
+
109
+ if ( element && isHTMLElement ( element ) ) {
110
+ if ( this . isEmpty ( ) ) {
111
+ element . append ( document . createElement ( 'br' ) ) ;
112
+ }
113
+ }
114
+
115
+ return {
116
+ element,
117
+ } ;
118
+ }
119
+
120
+ static importJSON ( serializedNode : SerializedHeadingNode ) : HeadingNode {
121
+ const node = $createHeadingNode ( serializedNode . tag ) ;
122
+ deserializeCommonBlockNode ( serializedNode , node ) ;
123
+ return node ;
124
+ }
125
+
126
+ exportJSON ( ) : SerializedHeadingNode {
127
+ return {
128
+ ...super . exportJSON ( ) ,
129
+ tag : this . getTag ( ) ,
130
+ type : 'heading' ,
131
+ version : 1 ,
132
+ } ;
133
+ }
134
+
135
+ // Mutation
136
+ insertNewAfter (
137
+ selection ?: RangeSelection ,
138
+ restoreSelection = true ,
139
+ ) : ParagraphNode | HeadingNode {
140
+ const anchorOffet = selection ? selection . anchor . offset : 0 ;
141
+ const lastDesc = this . getLastDescendant ( ) ;
142
+ const isAtEnd =
143
+ ! lastDesc ||
144
+ ( selection &&
145
+ selection . anchor . key === lastDesc . getKey ( ) &&
146
+ anchorOffet === lastDesc . getTextContentSize ( ) ) ;
147
+ const newElement =
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 ( ) ;
156
+ paragraph . select ( ) ;
157
+ this . replace ( paragraph , true ) ;
158
+ }
159
+ return newElement ;
160
+ }
161
+
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 ) ;
169
+ return true ;
170
+ }
171
+
172
+ extractWithChild ( ) : boolean {
173
+ return true ;
174
+ }
175
+ }
176
+
177
+ function $convertHeadingElement ( element : HTMLElement ) : DOMConversionOutput {
178
+ const nodeName = element . nodeName . toLowerCase ( ) ;
179
+ let node = null ;
180
+ if (
181
+ nodeName === 'h1' ||
182
+ nodeName === 'h2' ||
183
+ nodeName === 'h3' ||
184
+ nodeName === 'h4' ||
185
+ nodeName === 'h5' ||
186
+ nodeName === 'h6'
187
+ ) {
188
+ node = $createHeadingNode ( nodeName ) ;
189
+ setCommonBlockPropsFromElement ( element , node ) ;
190
+ }
191
+ return { node} ;
192
+ }
193
+
194
+ export function $createHeadingNode ( headingTag : HeadingTagType ) : HeadingNode {
195
+ return $applyNodeReplacement ( new HeadingNode ( headingTag ) ) ;
196
+ }
197
+
198
+ export function $isHeadingNode (
199
+ node : LexicalNode | null | undefined ,
200
+ ) : node is HeadingNode {
201
+ return node instanceof HeadingNode ;
202
+ }
0 commit comments