Skip to content

Commit ebd4604

Browse files
committed
Lexical: Merged list nodes
1 parent 36a4d79 commit ebd4604

File tree

10 files changed

+111
-492
lines changed

10 files changed

+111
-492
lines changed

resources/js/wysiwyg/lexical/list/LexicalListItemNode.ts

Lines changed: 43 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import type {
1313
DOMConversionOutput,
1414
DOMExportOutput,
1515
EditorConfig,
16-
EditorThemeClasses,
1716
LexicalNode,
1817
NodeKey,
1918
ParagraphNode,
@@ -22,10 +21,6 @@ import type {
2221
Spread,
2322
} from 'lexical';
2423

25-
import {
26-
addClassNamesToElement,
27-
removeClassNamesFromElement,
28-
} from '@lexical/utils';
2924
import {
3025
$applyNodeReplacement,
3126
$createParagraphNode,
@@ -36,11 +31,11 @@ import {
3631
LexicalEditor,
3732
} from 'lexical';
3833
import invariant from 'lexical/shared/invariant';
39-
import normalizeClassNames from 'lexical/shared/normalizeClassNames';
4034

4135
import {$createListNode, $isListNode} from './';
42-
import {$handleIndent, $handleOutdent, mergeLists} from './formatList';
36+
import {mergeLists} from './formatList';
4337
import {isNestedListNode} from './utils';
38+
import {el} from "../../utils/dom";
4439

4540
export type SerializedListItemNode = Spread<
4641
{
@@ -74,11 +69,17 @@ export class ListItemNode extends ElementNode {
7469
createDOM(config: EditorConfig): HTMLElement {
7570
const element = document.createElement('li');
7671
const parent = this.getParent();
72+
7773
if ($isListNode(parent) && parent.getListType() === 'check') {
78-
updateListItemChecked(element, this, null, parent);
74+
updateListItemChecked(element, this);
7975
}
76+
8077
element.value = this.__value;
81-
$setListItemThemeClassNames(element, config.theme, this);
78+
79+
if ($hasNestedListWithoutLabel(this)) {
80+
element.style.listStyle = 'none';
81+
}
82+
8283
return element;
8384
}
8485

@@ -89,11 +90,12 @@ export class ListItemNode extends ElementNode {
8990
): boolean {
9091
const parent = this.getParent();
9192
if ($isListNode(parent) && parent.getListType() === 'check') {
92-
updateListItemChecked(dom, this, prevNode, parent);
93+
updateListItemChecked(dom, this);
9394
}
95+
96+
dom.style.listStyle = $hasNestedListWithoutLabel(this) ? 'none' : '';
9497
// @ts-expect-error - this is always HTMLListItemElement
9598
dom.value = this.__value;
96-
$setListItemThemeClassNames(dom, config.theme, this);
9799

98100
return false;
99101
}
@@ -132,6 +134,20 @@ export class ListItemNode extends ElementNode {
132134

133135
exportDOM(editor: LexicalEditor): DOMExportOutput {
134136
const element = this.createDOM(editor._config);
137+
138+
if (element.classList.contains('task-list-item')) {
139+
const input = el('input', {
140+
type: 'checkbox',
141+
disabled: 'disabled',
142+
});
143+
if (element.hasAttribute('checked')) {
144+
input.setAttribute('checked', 'checked');
145+
element.removeAttribute('checked');
146+
}
147+
148+
element.prepend(input);
149+
}
150+
135151
return {
136152
element,
137153
};
@@ -390,89 +406,33 @@ export class ListItemNode extends ElementNode {
390406
}
391407
}
392408

393-
function $setListItemThemeClassNames(
394-
dom: HTMLElement,
395-
editorThemeClasses: EditorThemeClasses,
396-
node: ListItemNode,
397-
): void {
398-
const classesToAdd = [];
399-
const classesToRemove = [];
400-
const listTheme = editorThemeClasses.list;
401-
const listItemClassName = listTheme ? listTheme.listitem : undefined;
402-
let nestedListItemClassName;
403-
404-
if (listTheme && listTheme.nested) {
405-
nestedListItemClassName = listTheme.nested.listitem;
406-
}
407-
408-
if (listItemClassName !== undefined) {
409-
classesToAdd.push(...normalizeClassNames(listItemClassName));
410-
}
411-
412-
if (listTheme) {
413-
const parentNode = node.getParent();
414-
const isCheckList =
415-
$isListNode(parentNode) && parentNode.getListType() === 'check';
416-
const checked = node.getChecked();
417-
418-
if (!isCheckList || checked) {
419-
classesToRemove.push(listTheme.listitemUnchecked);
420-
}
421-
422-
if (!isCheckList || !checked) {
423-
classesToRemove.push(listTheme.listitemChecked);
424-
}
409+
function $hasNestedListWithoutLabel(node: ListItemNode): boolean {
410+
const children = node.getChildren();
411+
let hasLabel = false;
412+
let hasNestedList = false;
425413

426-
if (isCheckList) {
427-
classesToAdd.push(
428-
checked ? listTheme.listitemChecked : listTheme.listitemUnchecked,
429-
);
414+
for (const child of children) {
415+
if ($isListNode(child)) {
416+
hasNestedList = true;
417+
} else if (child.getTextContent().trim().length > 0) {
418+
hasLabel = true;
430419
}
431420
}
432421

433-
if (nestedListItemClassName !== undefined) {
434-
const nestedListItemClasses = normalizeClassNames(nestedListItemClassName);
435-
436-
if (node.getChildren().some((child) => $isListNode(child))) {
437-
classesToAdd.push(...nestedListItemClasses);
438-
} else {
439-
classesToRemove.push(...nestedListItemClasses);
440-
}
441-
}
442-
443-
if (classesToRemove.length > 0) {
444-
removeClassNamesFromElement(dom, ...classesToRemove);
445-
}
446-
447-
if (classesToAdd.length > 0) {
448-
addClassNamesToElement(dom, ...classesToAdd);
449-
}
422+
return hasNestedList && !hasLabel;
450423
}
451424

452425
function updateListItemChecked(
453426
dom: HTMLElement,
454427
listItemNode: ListItemNode,
455-
prevListItemNode: ListItemNode | null,
456-
listNode: ListNode,
457428
): void {
458-
// Only add attributes for leaf list items
459-
if ($isListNode(listItemNode.getFirstChild())) {
460-
dom.removeAttribute('role');
461-
dom.removeAttribute('tabIndex');
462-
dom.removeAttribute('aria-checked');
429+
// Only set task list attrs for leaf list items
430+
const shouldBeTaskItem = !$isListNode(listItemNode.getFirstChild());
431+
dom.classList.toggle('task-list-item', shouldBeTaskItem);
432+
if (listItemNode.__checked) {
433+
dom.setAttribute('checked', 'checked');
463434
} else {
464-
dom.setAttribute('role', 'checkbox');
465-
dom.setAttribute('tabIndex', '-1');
466-
467-
if (
468-
!prevListItemNode ||
469-
listItemNode.__checked !== prevListItemNode.__checked
470-
) {
471-
dom.setAttribute(
472-
'aria-checked',
473-
listItemNode.getChecked() ? 'true' : 'false',
474-
);
475-
}
435+
dom.removeAttribute('checked');
476436
}
477437
}
478438

resources/js/wysiwyg/lexical/list/LexicalListNode.ts

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@ import {
3636
updateChildrenListItemValue,
3737
} from './formatList';
3838
import {$getListDepth, $wrapInListItem} from './utils';
39+
import {extractDirectionFromElement} from "../../nodes/_common";
3940

4041
export type SerializedListNode = Spread<
4142
{
43+
id: string;
4244
listType: ListType;
4345
start: number;
4446
tag: ListNodeTagType;
@@ -58,15 +60,18 @@ export class ListNode extends ElementNode {
5860
__start: number;
5961
/** @internal */
6062
__listType: ListType;
63+
/** @internal */
64+
__id: string = '';
6165

6266
static getType(): string {
6367
return 'list';
6468
}
6569

6670
static clone(node: ListNode): ListNode {
67-
const listType = node.__listType || TAG_TO_LIST_TYPE[node.__tag];
68-
69-
return new ListNode(listType, node.__start, node.__key);
71+
const newNode = new ListNode(node.__listType, node.__start, node.__key);
72+
newNode.__id = node.__id;
73+
newNode.__dir = node.__dir;
74+
return newNode;
7075
}
7176

7277
constructor(listType: ListType, start: number, key?: NodeKey) {
@@ -81,6 +86,16 @@ export class ListNode extends ElementNode {
8186
return this.__tag;
8287
}
8388

89+
setId(id: string) {
90+
const self = this.getWritable();
91+
self.__id = id;
92+
}
93+
94+
getId(): string {
95+
const self = this.getLatest();
96+
return self.__id;
97+
}
98+
8499
setListType(type: ListType): void {
85100
const writable = this.getWritable();
86101
writable.__listType = type;
@@ -108,6 +123,14 @@ export class ListNode extends ElementNode {
108123
dom.__lexicalListType = this.__listType;
109124
$setListThemeClassNames(dom, config.theme, this);
110125

126+
if (this.__id) {
127+
dom.setAttribute('id', this.__id);
128+
}
129+
130+
if (this.__dir) {
131+
dom.setAttribute('dir', this.__dir);
132+
}
133+
111134
return dom;
112135
}
113136

@@ -116,7 +139,11 @@ export class ListNode extends ElementNode {
116139
dom: HTMLElement,
117140
config: EditorConfig,
118141
): boolean {
119-
if (prevNode.__tag !== this.__tag) {
142+
if (
143+
prevNode.__tag !== this.__tag
144+
|| prevNode.__dir !== this.__dir
145+
|| prevNode.__id !== this.__id
146+
) {
120147
return true;
121148
}
122149

@@ -148,8 +175,7 @@ export class ListNode extends ElementNode {
148175

149176
static importJSON(serializedNode: SerializedListNode): ListNode {
150177
const node = $createListNode(serializedNode.listType, serializedNode.start);
151-
node.setFormat(serializedNode.format);
152-
node.setIndent(serializedNode.indent);
178+
node.setId(serializedNode.id);
153179
node.setDirection(serializedNode.direction);
154180
return node;
155181
}
@@ -177,6 +203,7 @@ export class ListNode extends ElementNode {
177203
tag: this.getTag(),
178204
type: 'list',
179205
version: 1,
206+
id: this.__id,
180207
};
181208
}
182209

@@ -277,28 +304,21 @@ function $setListThemeClassNames(
277304
}
278305

279306
/*
280-
* This function normalizes the children of a ListNode after the conversion from HTML,
281-
* ensuring that they are all ListItemNodes and contain either a single nested ListNode
282-
* or some other inline content.
307+
* This function is a custom normalization function to allow nested lists within list item elements.
308+
* Original taken from https://github.com/facebook/lexical/blob/6e10210fd1e113ccfafdc999b1d896733c5c5bea/packages/lexical-list/src/LexicalListNode.ts#L284-L303
309+
* With modifications made.
283310
*/
284311
function $normalizeChildren(nodes: Array<LexicalNode>): Array<ListItemNode> {
285312
const normalizedListItems: Array<ListItemNode> = [];
286-
for (let i = 0; i < nodes.length; i++) {
287-
const node = nodes[i];
313+
314+
for (const node of nodes) {
288315
if ($isListItemNode(node)) {
289316
normalizedListItems.push(node);
290-
const children = node.getChildren();
291-
if (children.length > 1) {
292-
children.forEach((child) => {
293-
if ($isListNode(child)) {
294-
normalizedListItems.push($wrapInListItem(child));
295-
}
296-
});
297-
}
298317
} else {
299318
normalizedListItems.push($wrapInListItem(node));
300319
}
301320
}
321+
302322
return normalizedListItems;
303323
}
304324

@@ -334,6 +354,14 @@ function $convertListNode(domNode: HTMLElement): DOMConversionOutput {
334354
}
335355
}
336356

357+
if (domNode.id && node) {
358+
node.setId(domNode.id);
359+
}
360+
361+
if (domNode.dir && node) {
362+
node.setDirection(extractDirectionFromElement(domNode));
363+
}
364+
337365
return {
338366
after: $normalizeChildren,
339367
node,

0 commit comments

Comments
 (0)