Skip to content

Commit 36a4d79

Browse files
committed
Lexical: Extracted & merged heading & quote nodes
1 parent f3fa63a commit 36a4d79

File tree

20 files changed

+370
-651
lines changed

20 files changed

+370
-651
lines changed

resources/js/wysiwyg/lexical/core/__tests__/unit/LexicalSerialization.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88

99
import {$createLinkNode} from '@lexical/link';
1010
import {$createListItemNode, $createListNode} from '@lexical/list';
11-
import {$createHeadingNode, $createQuoteNode} from '@lexical/rich-text';
1211
import {$createTableNodeWithDimensions} from '@lexical/table';
1312
import {$createParagraphNode, $createTextNode, $getRoot} from 'lexical';
1413

1514
import {initializeUnitTest} from '../utils';
15+
import {$createHeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
16+
import {$createQuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
1617

1718
function $createEditorContent() {
1819
const root = $getRoot();

resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {createHeadlessEditor} from '@lexical/headless';
1010
import {AutoLinkNode, LinkNode} from '@lexical/link';
1111
import {ListItemNode, ListNode} from '@lexical/list';
1212

13-
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
1413
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
1514

1615
import {
@@ -36,6 +35,8 @@ import {
3635
LexicalNodeReplacement,
3736
} from '../../LexicalEditor';
3837
import {resetRandomKey} from '../../LexicalUtils';
38+
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
39+
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
3940

4041

4142
type TestEnv = {

resources/js/wysiwyg/lexical/core/nodes/CommonBlockNode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class CommonBlockNode extends ElementNode {
4848
}
4949

5050
export function copyCommonBlockProperties(from: CommonBlockNode, to: CommonBlockNode): void {
51-
to.__id = from.__id;
51+
// to.__id = from.__id;
5252
to.__alignment = from.__alignment;
5353
to.__inset = from.__inset;
5454
}

resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalTabNode.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
$insertDataTransferForRichText,
1212
} from '@lexical/clipboard';
1313
import {$createListItemNode, $createListNode} from '@lexical/list';
14-
import {$createHeadingNode, registerRichText} from '@lexical/rich-text';
14+
import {registerRichText} from '@lexical/rich-text';
1515
import {
1616
$createParagraphNode,
1717
$createRangeSelection,
@@ -32,6 +32,7 @@ import {
3232
initializeUnitTest,
3333
invariant,
3434
} from '../../../__tests__/utils';
35+
import {$createHeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
3536

3637
describe('LexicalTabNode tests', () => {
3738
initializeUnitTest((testEnv) => {

resources/js/wysiwyg/lexical/html/__tests__/unit/LexicalHtml.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ import {createHeadlessEditor} from '@lexical/headless';
1313
import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';
1414
import {LinkNode} from '@lexical/link';
1515
import {ListItemNode, ListNode} from '@lexical/list';
16-
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
1716
import {
1817
$createParagraphNode,
1918
$createRangeSelection,
2019
$createTextNode,
2120
$getRoot,
2221
} from 'lexical';
22+
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
23+
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
2324

2425
describe('HTML', () => {
2526
type Input = Array<{
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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

Comments
 (0)