X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/e65655594f8007d44515a23867e54eb8321a8ead..d145efb6f6eac73947c7f3c53173f42b4dd9a615:/resources/js/wysiwyg/utils/lists.ts diff --git a/resources/js/wysiwyg/utils/lists.ts b/resources/js/wysiwyg/utils/lists.ts index edde994e5..3deb9dfb6 100644 --- a/resources/js/wysiwyg/utils/lists.ts +++ b/resources/js/wysiwyg/utils/lists.ts @@ -1,22 +1,24 @@ -import {$createCustomListItemNode, $isCustomListItemNode, CustomListItemNode} from "../nodes/custom-list-item"; -import {$createCustomListNode, $isCustomListNode} from "../nodes/custom-list"; -import {BaseSelection, LexicalEditor} from "lexical"; -import {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection, getLastSelection} from "./selection"; -import {nodeHasInset} from "./nodes"; +import {$createTextNode, $getSelection, BaseSelection, LexicalEditor, TextNode} from "lexical"; +import {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection} from "./selection"; +import {$sortNodes, nodeHasInset} from "./nodes"; +import {$createListItemNode, $createListNode, $isListItemNode, $isListNode, ListItemNode} from "@lexical/list"; -export function $nestListItem(node: CustomListItemNode) { +export function $nestListItem(node: ListItemNode): ListItemNode { const list = node.getParent(); - if (!$isCustomListNode(list)) { - return; + if (!$isListNode(list)) { + return node; } - const listItems = list.getChildren() as CustomListItemNode[]; + const nodeChildList = node.getChildren().filter(n => $isListNode(n))[0] || null; + const nodeChildItems = nodeChildList?.getChildren() || []; + + const listItems = list.getChildren() as ListItemNode[]; const nodeIndex = listItems.findIndex((n) => n.getKey() === node.getKey()); const isFirst = nodeIndex === 0; - const newListItem = $createCustomListItemNode(); - const newList = $createCustomListNode(list.getListType()); + const newListItem = $createListItemNode(); + const newList = $createListNode(list.getListType()); newList.append(newListItem); newListItem.append(...node.getChildren()); @@ -27,40 +29,89 @@ export function $nestListItem(node: CustomListItemNode) { prevListItem.append(newList); node.remove(); } + + if (nodeChildList) { + for (const child of nodeChildItems) { + newListItem.insertAfter(child); + } + nodeChildList.remove(); + } + + return newListItem; } -export function $unnestListItem(node: CustomListItemNode) { +export function $unnestListItem(node: ListItemNode): ListItemNode { const list = node.getParent(); const parentListItem = list?.getParent(); const outerList = parentListItem?.getParent(); - if (!$isCustomListNode(list) || !$isCustomListNode(outerList) || !$isCustomListItemNode(parentListItem)) { - return; + if (!$isListNode(list) || !$isListNode(outerList) || !$isListItemNode(parentListItem)) { + return node; } + const laterSiblings = node.getNextSiblings(); parentListItem.insertAfter(node); if (list.getChildren().length === 0) { list.remove(); } + if (laterSiblings.length > 0) { + const childList = $createListNode(list.getListType()); + childList.append(...laterSiblings); + node.append(childList); + } + + if (list.getChildrenSize() === 0) { + list.remove(); + } + if (parentListItem.getChildren().length === 0) { parentListItem.remove(); } + + return node; } -function getListItemsForSelection(selection: BaseSelection|null): (CustomListItemNode|null)[] { +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 ($isCustomListItemNode(node)) { - listItemNodes.push(node); + if ($isListItemNode(node)) { + if (!itemsToIgnore.has(node.getKey())) { + listItemNodes.push(node); + } continue; } const parents = node.getParents(); for (const parent of parents) { - if ($isCustomListItemNode(parent)) { - listItemNodes.push(parent); + if ($isListItemNode(parent)) { + if (!itemsToIgnore.has(parent.getKey())) { + listItemNodes.push(parent); + } continue outer; } } @@ -71,8 +122,8 @@ function getListItemsForSelection(selection: BaseSelection|null): (CustomListIte return listItemNodes; } -function $reduceDedupeListItems(listItems: (CustomListItemNode|null)[]): CustomListItemNode[] { - const listItemMap: Record = {}; +function $reduceDedupeListItems(listItems: (ListItemNode|null)[]): ListItemNode[] { + const listItemMap: Record = {}; for (const item of listItems) { if (item === null) { @@ -85,28 +136,43 @@ function $reduceDedupeListItems(listItems: (CustomListItemNode|null)[]): CustomL } } - return Object.values(listItemMap); + const items = Object.values(listItemMap); + return $sortNodes(items) as ListItemNode[]; } export function $setInsetForSelection(editor: LexicalEditor, change: number): void { - const selection = getLastSelection(editor); - + const selection = $getSelection(); + const selectionBounds = selection?.getStartEndPoints(); const listItemsInSelection = getListItemsForSelection(selection); const isListSelection = listItemsInSelection.length > 0 && !listItemsInSelection.includes(null); if (isListSelection) { + const alteredListItems = []; const listItems = $reduceDedupeListItems(listItemsInSelection); if (change > 0) { for (const listItem of listItems) { - $nestListItem(listItem); + alteredListItems.push($nestListItem(listItem)); } } else if (change < 0) { for (const listItem of [...listItems].reverse()) { - $unnestListItem(listItem); + alteredListItems.push($unnestListItem(listItem)); + } + alteredListItems.reverse(); + } + + if (alteredListItems.length === 1 && selectionBounds) { + // Retain selection range if moving just one item + const listItem = alteredListItems[0] as ListItemNode; + let child = listItem.getChildren()[0] as TextNode; + if (!child) { + child = $createTextNode(''); + listItem.append(child); } + child.select(selectionBounds[0].offset, selectionBounds[1].offset); + } else { + $selectNodes(alteredListItems); } - $selectNodes(listItems); return; }