Skip to content

Commit 62c8eb3

Browse files
committed
Lexical: Made list selections & intendting more reliable
- Added handling to not include parent of top-most list range selection so that it's not also changed while not visually part of the selection range. - Fixed issue where list items could be left over after unnesting, due to empty checks/removals occuring before all child handling. - Added node sorting, applied to list items during nest operations so that selection range remains reliable.
1 parent c03e441 commit 62c8eb3

File tree

2 files changed

+61
-10
lines changed

2 files changed

+61
-10
lines changed

resources/js/wysiwyg/utils/lists.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {$createTextNode, $getSelection, BaseSelection, LexicalEditor, TextNode} from "lexical";
22
import {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection} from "./selection";
3-
import {nodeHasInset} from "./nodes";
3+
import {$sortNodes, nodeHasInset} from "./nodes";
44
import {$createListItemNode, $createListNode, $isListItemNode, $isListNode, ListItemNode} from "@lexical/list";
55

66

@@ -49,16 +49,11 @@ export function $unnestListItem(node: ListItemNode): ListItemNode {
4949
}
5050

5151
const laterSiblings = node.getNextSiblings();
52-
5352
parentListItem.insertAfter(node);
5453
if (list.getChildren().length === 0) {
5554
list.remove();
5655
}
5756

58-
if (parentListItem.getChildren().length === 0) {
59-
parentListItem.remove();
60-
}
61-
6257
if (laterSiblings.length > 0) {
6358
const childList = $createListNode(list.getListType());
6459
childList.append(...laterSiblings);
@@ -69,23 +64,54 @@ export function $unnestListItem(node: ListItemNode): ListItemNode {
6964
list.remove();
7065
}
7166

67+
if (parentListItem.getChildren().length === 0) {
68+
parentListItem.remove();
69+
}
70+
7271
return node;
7372
}
7473

7574
function getListItemsForSelection(selection: BaseSelection|null): (ListItemNode|null)[] {
7675
const nodes = selection?.getNodes() || [];
77-
const listItemNodes = [];
76+
let [start, end] = selection?.getStartEndPoints() || [null, null];
77+
78+
// Ensure we ignore parent list items of the top-most list item since,
79+
// although technically part of the selection, from a user point of
80+
// view the selection does not spread to encompass this outer element.
81+
const itemsToIgnore: Set<string> = new Set();
82+
if (selection && start) {
83+
if (selection.isBackward() && end) {
84+
[end, start] = [start, end];
85+
}
7886

87+
const startParents = start.getNode().getParents();
88+
let foundList = false;
89+
for (const parent of startParents) {
90+
if ($isListItemNode(parent)) {
91+
if (foundList) {
92+
itemsToIgnore.add(parent.getKey());
93+
} else {
94+
foundList = true;
95+
}
96+
}
97+
}
98+
}
99+
100+
const listItemNodes = [];
79101
outer: for (const node of nodes) {
80102
if ($isListItemNode(node)) {
81-
listItemNodes.push(node);
103+
if (!itemsToIgnore.has(node.getKey())) {
104+
listItemNodes.push(node);
105+
}
82106
continue;
83107
}
84108

85109
const parents = node.getParents();
86110
for (const parent of parents) {
87111
if ($isListItemNode(parent)) {
88-
listItemNodes.push(parent);
112+
if (!itemsToIgnore.has(parent.getKey())) {
113+
listItemNodes.push(parent);
114+
}
89115
continue outer;
90116
}
91117
}
@@ -110,7 +136,8 @@ function $reduceDedupeListItems(listItems: (ListItemNode|null)[]): ListItemNode[
110136
}
111137
}
112138

113-
return Object.values(listItemMap);
139+
const items = Object.values(listItemMap);
140+
return $sortNodes(items) as ListItemNode[];
114141
}
115142

116143
export function $setInsetForSelection(editor: LexicalEditor, change: number): void {

resources/js/wysiwyg/utils/nodes.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,30 @@ export function $getNearestNodeBlockParent(node: LexicalNode): LexicalNode|null
9494
return $findMatchingParent(node, isBlockNode);
9595
}
9696

97+
export function $sortNodes(nodes: LexicalNode[]): LexicalNode[] {
98+
const idChain: string[] = [];
99+
const addIds = (n: ElementNode) => {
100+
for (const child of n.getChildren()) {
101+
idChain.push(child.getKey())
102+
if ($isElementNode(child)) {
103+
addIds(child)
104+
}
105+
}
106+
};
107+
108+
const root = $getRoot();
109+
addIds(root);
110+
111+
const sorted = Array.from(nodes);
112+
sorted.sort((a, b) => {
113+
const aIndex = idChain.indexOf(a.getKey());
114+
const bIndex = idChain.indexOf(b.getKey());
115+
return aIndex - bIndex;
116+
});
117+
118+
return sorted;
119+
}
120+
97121
export function nodeHasAlignment(node: object): node is NodeHasAlignment {
98122
return '__alignment' in node;
99123
}

0 commit comments

Comments
 (0)