outdentListItemToParagraph function
Outdent of a ListItem at the first level: transforms into paragraph. Removes the item from the list, takes the first Paragraph as content, and promotes the other children (images, tables, sublists) to the parent level. Returns the created Paragraph or null if the operation fails.
Implementation
Paragraph? outdentListItemToParagraph(
Root root,
FluentList listParent,
ListItem currentItem,
) {
// Find the parent of the list (where to insert the item after outdent)
final grandparent = findParent(root, listParent);
if (grandparent == null) return null;
// Snapshot of children before removing the item
final itemChildren = currentItem.children.toList();
// Extract the first Paragraph (which will become the new output paragraph)
// and the other children that need to be promoted.
Paragraph? firstParagraph;
final otherChildren = <FNode>[];
for (final c in itemChildren) {
if (firstParagraph == null && c is Paragraph) {
firstParagraph = c;
} else {
otherChildren.add(c);
}
}
// Capture the items that come AFTER currentItem in the list:
// they will go in a new separate list after the paragraph.
final currentIndex = listParent.items.indexOf(currentItem);
final itemsAfter = (currentIndex >= 0)
? listParent.items.sublist(currentIndex + 1).toList()
: <ListItem>[];
// Remove the current item from the list
removeNode(root, currentItem);
// Also remove the following items from the original list
for (final item in itemsAfter) {
removeNode(root, item);
}
// Detach the promoted children from the orphaned currentItem: so the nodes
// exist ONLY in their new position in Root, avoiding double references
// that confuse widget tree reconciliation and ParagraphRegistry during
// cursor navigation.
currentItem.children.clear();
// If there's no Paragraph, create an empty one
final newParagraph = firstParagraph ?? Paragraph();
// Always insert the paragraph after the list (Google Docs behavior):
// - If the list has other items, the paragraph goes after it
// - If the list is empty (outdent of the last item), the paragraph goes after the empty list
// (the empty list will be removed after if necessary)
insertAfter(grandparent, listParent, newParagraph);
// Promote the other children of the ListItem (images, sublists, etc.)
// immediately after the paragraph.
var insertAfterNode = newParagraph as FNode;
for (final child in otherChildren) {
if (child is FluentList) {
_promoteSublistRecursive(root, grandparent, insertAfterNode, child);
} else {
insertAfter(grandparent, insertAfterNode, child);
}
insertAfterNode = child;
}
// If there are following items, create a new separate list after the paragraph
if (itemsAfter.isNotEmpty) {
final newList = FluentList(listType: listParent.listType);
for (final item in itemsAfter) {
appendChild(newList, item);
}
insertAfter(grandparent, insertAfterNode, newList);
}
// If the original list remained empty, remove it to avoid empty orphan lists
if (listParent.items.isEmpty) {
removeNode(root, listParent);
}
// Merge consecutive lists with the same type
mergeConsecutiveLists(root);
// Recalculate the indices of all modified lists
recalculateListIndices(root);
return newParagraph;
}