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.
This commit is contained in:
Dan Brown 2025-03-27 17:49:48 +00:00
parent c03e44124a
commit 62c8eb3357
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
2 changed files with 61 additions and 10 deletions

View File

@ -1,6 +1,6 @@
import {$createTextNode, $getSelection, BaseSelection, LexicalEditor, TextNode} from "lexical"; import {$createTextNode, $getSelection, BaseSelection, LexicalEditor, TextNode} from "lexical";
import {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection} from "./selection"; import {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection} from "./selection";
import {nodeHasInset} from "./nodes"; import {$sortNodes, nodeHasInset} from "./nodes";
import {$createListItemNode, $createListNode, $isListItemNode, $isListNode, ListItemNode} from "@lexical/list"; import {$createListItemNode, $createListNode, $isListItemNode, $isListNode, ListItemNode} from "@lexical/list";
@ -49,16 +49,11 @@ export function $unnestListItem(node: ListItemNode): ListItemNode {
} }
const laterSiblings = node.getNextSiblings(); const laterSiblings = node.getNextSiblings();
parentListItem.insertAfter(node); parentListItem.insertAfter(node);
if (list.getChildren().length === 0) { if (list.getChildren().length === 0) {
list.remove(); list.remove();
} }
if (parentListItem.getChildren().length === 0) {
parentListItem.remove();
}
if (laterSiblings.length > 0) { if (laterSiblings.length > 0) {
const childList = $createListNode(list.getListType()); const childList = $createListNode(list.getListType());
childList.append(...laterSiblings); childList.append(...laterSiblings);
@ -69,23 +64,54 @@ export function $unnestListItem(node: ListItemNode): ListItemNode {
list.remove(); list.remove();
} }
if (parentListItem.getChildren().length === 0) {
parentListItem.remove();
}
return node; return node;
} }
function getListItemsForSelection(selection: BaseSelection|null): (ListItemNode|null)[] { function getListItemsForSelection(selection: BaseSelection|null): (ListItemNode|null)[] {
const nodes = selection?.getNodes() || []; const nodes = selection?.getNodes() || [];
const listItemNodes = []; let [start, end] = selection?.getStartEndPoints() || [null, null];
// Ensure we ignore parent list items of the top-most list item since,
// although technically part of the selection, from a user point of
// view the selection does not spread to encompass this outer element.
const itemsToIgnore: Set<string> = new Set();
if (selection && start) {
if (selection.isBackward() && end) {
[end, start] = [start, end];
}
const startParents = start.getNode().getParents();
let foundList = false;
for (const parent of startParents) {
if ($isListItemNode(parent)) {
if (foundList) {
itemsToIgnore.add(parent.getKey());
} else {
foundList = true;
}
}
}
}
const listItemNodes = [];
outer: for (const node of nodes) { outer: for (const node of nodes) {
if ($isListItemNode(node)) { if ($isListItemNode(node)) {
listItemNodes.push(node); if (!itemsToIgnore.has(node.getKey())) {
listItemNodes.push(node);
}
continue; continue;
} }
const parents = node.getParents(); const parents = node.getParents();
for (const parent of parents) { for (const parent of parents) {
if ($isListItemNode(parent)) { if ($isListItemNode(parent)) {
listItemNodes.push(parent); if (!itemsToIgnore.has(parent.getKey())) {
listItemNodes.push(parent);
}
continue outer; continue outer;
} }
} }
@ -110,7 +136,8 @@ function $reduceDedupeListItems(listItems: (ListItemNode|null)[]): ListItemNode[
} }
} }
return Object.values(listItemMap); const items = Object.values(listItemMap);
return $sortNodes(items) as ListItemNode[];
} }
export function $setInsetForSelection(editor: LexicalEditor, change: number): void { export function $setInsetForSelection(editor: LexicalEditor, change: number): void {

View File

@ -94,6 +94,30 @@ export function $getNearestNodeBlockParent(node: LexicalNode): LexicalNode|null
return $findMatchingParent(node, isBlockNode); return $findMatchingParent(node, isBlockNode);
} }
export function $sortNodes(nodes: LexicalNode[]): LexicalNode[] {
const idChain: string[] = [];
const addIds = (n: ElementNode) => {
for (const child of n.getChildren()) {
idChain.push(child.getKey())
if ($isElementNode(child)) {
addIds(child)
}
}
};
const root = $getRoot();
addIds(root);
const sorted = Array.from(nodes);
sorted.sort((a, b) => {
const aIndex = idChain.indexOf(a.getKey());
const bIndex = idChain.indexOf(b.getKey());
return aIndex - bIndex;
});
return sorted;
}
export function nodeHasAlignment(node: object): node is NodeHasAlignment { export function nodeHasAlignment(node: object): node is NodeHasAlignment {
return '__alignment' in node; return '__alignment' in node;
} }