mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-01-10 02:57:36 +00:00
f4005a139b
Sibling/child items will now remain at the same visual level during nesting/un-nested, so only the selected item level is visually altered. Also added new model-based editor content matching system for tests.
162 lines
No EOL
4.9 KiB
TypeScript
162 lines
No EOL
4.9 KiB
TypeScript
import {$createTextNode, $getSelection, BaseSelection, LexicalEditor, TextNode} from "lexical";
|
|
import {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection} from "./selection";
|
|
import {nodeHasInset} from "./nodes";
|
|
import {$createListItemNode, $createListNode, $isListItemNode, $isListNode, ListItemNode} from "@lexical/list";
|
|
|
|
|
|
export function $nestListItem(node: ListItemNode): ListItemNode {
|
|
const list = node.getParent();
|
|
if (!$isListNode(list)) {
|
|
return node;
|
|
}
|
|
|
|
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 = $createListItemNode();
|
|
const newList = $createListNode(list.getListType());
|
|
newList.append(newListItem);
|
|
newListItem.append(...node.getChildren());
|
|
|
|
if (isFirst) {
|
|
node.append(newList);
|
|
} else {
|
|
const prevListItem = listItems[nodeIndex - 1];
|
|
prevListItem.append(newList);
|
|
node.remove();
|
|
}
|
|
|
|
if (nodeChildList) {
|
|
for (const child of nodeChildItems) {
|
|
newListItem.insertAfter(child);
|
|
}
|
|
nodeChildList.remove();
|
|
}
|
|
|
|
return newListItem;
|
|
}
|
|
|
|
export function $unnestListItem(node: ListItemNode): ListItemNode {
|
|
const list = node.getParent();
|
|
const parentListItem = list?.getParent();
|
|
const outerList = parentListItem?.getParent();
|
|
if (!$isListNode(list) || !$isListNode(outerList) || !$isListItemNode(parentListItem)) {
|
|
return node;
|
|
}
|
|
|
|
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);
|
|
node.append(childList);
|
|
}
|
|
|
|
if (list.getChildrenSize() === 0) {
|
|
list.remove();
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
function getListItemsForSelection(selection: BaseSelection|null): (ListItemNode|null)[] {
|
|
const nodes = selection?.getNodes() || [];
|
|
const listItemNodes = [];
|
|
|
|
outer: for (const node of nodes) {
|
|
if ($isListItemNode(node)) {
|
|
listItemNodes.push(node);
|
|
continue;
|
|
}
|
|
|
|
const parents = node.getParents();
|
|
for (const parent of parents) {
|
|
if ($isListItemNode(parent)) {
|
|
listItemNodes.push(parent);
|
|
continue outer;
|
|
}
|
|
}
|
|
|
|
listItemNodes.push(null);
|
|
}
|
|
|
|
return listItemNodes;
|
|
}
|
|
|
|
function $reduceDedupeListItems(listItems: (ListItemNode|null)[]): ListItemNode[] {
|
|
const listItemMap: Record<string, ListItemNode> = {};
|
|
|
|
for (const item of listItems) {
|
|
if (item === null) {
|
|
continue;
|
|
}
|
|
|
|
const key = item.getKey();
|
|
if (typeof listItemMap[key] === 'undefined') {
|
|
listItemMap[key] = item;
|
|
}
|
|
}
|
|
|
|
return Object.values(listItemMap);
|
|
}
|
|
|
|
export function $setInsetForSelection(editor: LexicalEditor, change: number): void {
|
|
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) {
|
|
alteredListItems.push($nestListItem(listItem));
|
|
}
|
|
} else if (change < 0) {
|
|
for (const listItem of [...listItems].reverse()) {
|
|
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);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
const elements = $getBlockElementNodesInSelection(selection);
|
|
for (const node of elements) {
|
|
if (nodeHasInset(node)) {
|
|
const currentInset = node.getInset();
|
|
const newInset = Math.min(Math.max(currentInset + change, 0), 500);
|
|
node.setInset(newInset)
|
|
}
|
|
}
|
|
|
|
$toggleSelection(editor);
|
|
} |