From 62c8eb335785e41c730569ec34df7137f2f325f1 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 27 Mar 2025 17:49:48 +0000 Subject: [PATCH] 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. --- resources/js/wysiwyg/utils/lists.ts | 47 +++++++++++++++++++++++------ resources/js/wysiwyg/utils/nodes.ts | 24 +++++++++++++++ 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/resources/js/wysiwyg/utils/lists.ts b/resources/js/wysiwyg/utils/lists.ts index 005b05f98..3deb9dfb6 100644 --- a/resources/js/wysiwyg/utils/lists.ts +++ b/resources/js/wysiwyg/utils/lists.ts @@ -1,6 +1,6 @@ import {$createTextNode, $getSelection, BaseSelection, LexicalEditor, TextNode} from "lexical"; import {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection} from "./selection"; -import {nodeHasInset} from "./nodes"; +import {$sortNodes, nodeHasInset} from "./nodes"; import {$createListItemNode, $createListNode, $isListItemNode, $isListNode, ListItemNode} from "@lexical/list"; @@ -49,16 +49,11 @@ export function $unnestListItem(node: ListItemNode): ListItemNode { } const laterSiblings = node.getNextSiblings(); - parentListItem.insertAfter(node); if (list.getChildren().length === 0) { list.remove(); } - if (parentListItem.getChildren().length === 0) { - parentListItem.remove(); - } - if (laterSiblings.length > 0) { const childList = $createListNode(list.getListType()); childList.append(...laterSiblings); @@ -69,23 +64,54 @@ export function $unnestListItem(node: ListItemNode): ListItemNode { list.remove(); } + if (parentListItem.getChildren().length === 0) { + parentListItem.remove(); + } + return node; } function getListItemsForSelection(selection: BaseSelection|null): (ListItemNode|null)[] { 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 = 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) { if ($isListItemNode(node)) { - listItemNodes.push(node); + if (!itemsToIgnore.has(node.getKey())) { + listItemNodes.push(node); + } continue; } const parents = node.getParents(); for (const parent of parents) { if ($isListItemNode(parent)) { - listItemNodes.push(parent); + if (!itemsToIgnore.has(parent.getKey())) { + listItemNodes.push(parent); + } 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 { diff --git a/resources/js/wysiwyg/utils/nodes.ts b/resources/js/wysiwyg/utils/nodes.ts index b5cc78955..591232ea3 100644 --- a/resources/js/wysiwyg/utils/nodes.ts +++ b/resources/js/wysiwyg/utils/nodes.ts @@ -94,6 +94,30 @@ export function $getNearestNodeBlockParent(node: LexicalNode): LexicalNode|null 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 { return '__alignment' in node; }