Fix indented list behavior when pressing Return (#779)

* Fix indented list behavior when pressing Return.

* Fix test.

Co-authored-by: Acy Watson <acy@fb.com>
This commit is contained in:
Acy Watson
2021-10-29 10:04:08 -07:00
committed by acywatson
parent bd734db51d
commit 3b128e8e27
5 changed files with 231 additions and 5 deletions

View File

@ -316,7 +316,7 @@ describe('OutlineListItemNode tests', () => {
listItemNode1.insertNewAfter();
});
expect(testEnv.outerHTML).toBe(
'<div contenteditable="true" data-outline-editor="true"><p><br></p><ul><li><br></li><li><br></li></ul></div>',
'<div contenteditable="true" data-outline-editor="true"><ul><li><br></li><li><br></li><li><br></li><li><br></li></ul></div>',
);
});

View File

@ -20,6 +20,7 @@ import {isBlockNode, BlockNode} from 'outline';
import {createParagraphNode} from 'outline/ParagraphNode';
import {createListNode, isListNode} from 'outline/ListNode';
import invariant from 'shared/invariant';
import {getTopListNode, isLastItemInList} from 'outline/nodes';
export class ListItemNode extends BlockNode {
static clone(node: ListItemNode): ListItemNode {
@ -118,13 +119,16 @@ export class ListItemNode extends BlockNode {
insertNewAfter(): ListItemNode | ParagraphNode {
const nextSibling = this.getNextSibling();
const prevSibling = this.getPreviousSibling();
const list = this.getParent();
const list = getTopListNode(this);
const isLast = isLastItemInList(this);
let newBlock;
if (
isBlockNode(list) &&
this.getTextContent() === '' &&
(prevSibling === null || nextSibling === null)
(prevSibling === null || nextSibling === null) &&
isLast
) {
if (nextSibling === null) {
newBlock = createParagraphNode();

View File

@ -7,8 +7,13 @@
* @flow strict
*/
import type {ListItemNode} from 'outline/ListItemNode';
import type {OutlineNode, BlockNode} from 'outline';
import type {ListNode} from 'outline/ListNode';
import {isListNode} from 'outline/ListNode';
import {isListItemNode} from 'outline/ListItemNode';
import invariant from 'shared/invariant';
import {isBlockNode} from 'outline';
export function dfs(
@ -53,3 +58,36 @@ export function getCommonAncestor(nodes: OutlineNode[]): null | BlockNode {
}
return commonAncestor;
}
export function getTopListNode(listItem: ListItemNode): ListNode {
let list = listItem.getParent();
if (!isListNode(list)) {
invariant(false, 'A ListItemNode must have a ListNode for a parent.');
}
let parent = list;
while (parent !== null) {
parent = parent.getParent();
if (isListNode(parent)) {
list = parent;
}
}
return list;
}
export function isLastItemInList(listItem: ListItemNode): boolean {
let isLast = true;
const firstChild = listItem.getFirstChild();
if (isListNode(firstChild)) {
return false;
}
let parent = listItem;
while (parent !== null) {
if (isListItemNode(parent)) {
if (parent.getNextSiblings().length > 0) {
isLast = false;
}
}
parent = parent.getParent();
}
return isLast;
}

View File

@ -13,9 +13,11 @@ import {
initializeUnitTest,
createTestBlockNode,
} from '../../../__tests__/utils';
import {dfs} from '../../OutlineNodeHelpers';
import {dfs, getTopListNode, isLastItemInList} from 'outline/nodes';
import {createParagraphNode, isParagraphNode} from 'outline/ParagraphNode';
import {createTextNode} from 'outline';
import {createListNode} from 'outline/ListNode';
import {createListItemNode} from 'outline/ListItemNode';
describe('OutlineNodeHelpers tests', () => {
initializeUnitTest((testEnv) => {
@ -110,5 +112,186 @@ describe('OutlineNodeHelpers tests', () => {
});
expect(dfsKeys).toEqual(expectedKeys);
});
test('getTopListNode should return the top list node when the list is a direct child of the RootNode', async () => {
const editor: OutlineEditor = testEnv.editor;
await editor.update((view: View) => {
// Root
// |- ListNode
// |- ListItemNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
const root = view.getRoot();
const topListNode = createListNode('ul');
const secondLevelListNode = createListNode('ul');
const listItem1 = createListItemNode();
const listItem2 = createListItemNode();
const listItem3 = createListItemNode();
root.append(topListNode);
topListNode.append(listItem1);
topListNode.append(listItem2);
topListNode.append(secondLevelListNode);
secondLevelListNode.append(listItem3);
const result = getTopListNode(listItem3);
expect(result.getKey()).toEqual(topListNode.getKey());
});
});
test('getTopListNode should return the top list node when the list is not a direct child of the RootNode', async () => {
const editor: OutlineEditor = testEnv.editor;
await editor.update((view: View) => {
// Root
// |- ParagaphNode
// |- ListNode
// |- ListItemNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
const root = view.getRoot();
const paragraphNode = createParagraphNode();
const topListNode = createListNode('ul');
const secondLevelListNode = createListNode('ul');
const listItem1 = createListItemNode();
const listItem2 = createListItemNode();
const listItem3 = createListItemNode();
root.append(paragraphNode);
paragraphNode.append(topListNode);
topListNode.append(listItem1);
topListNode.append(listItem2);
topListNode.append(secondLevelListNode);
secondLevelListNode.append(listItem3);
const result = getTopListNode(listItem3);
expect(result.getKey()).toEqual(topListNode.getKey());
});
});
test('getTopListNode should return the top list node when the list item is deeply nested.', async () => {
const editor: OutlineEditor = testEnv.editor;
await editor.update((view: View) => {
// Root
// |- ParagaphNode
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
// |- ListItemNode
const root = view.getRoot();
const paragraphNode = createParagraphNode();
const topListNode = createListNode('ul');
const secondLevelListNode = createListNode('ul');
const thirdLevelListNode = createListNode('ul');
const listItem1 = createListItemNode();
const listItem2 = createListItemNode();
const listItem3 = createListItemNode();
const listItem4 = createListItemNode();
root.append(paragraphNode);
paragraphNode.append(topListNode);
topListNode.append(listItem1);
listItem1.append(secondLevelListNode);
secondLevelListNode.append(listItem2);
listItem2.append(thirdLevelListNode);
thirdLevelListNode.append(listItem3);
topListNode.append(listItem4);
const result = getTopListNode(listItem4);
expect(result.getKey()).toEqual(topListNode.getKey());
});
});
test('isLastItemInList should return true if the listItem is the last in a nested list.', async () => {
const editor: OutlineEditor = testEnv.editor;
await editor.update((view: View) => {
// Root
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
const root = view.getRoot();
const topListNode = createListNode('ul');
const secondLevelListNode = createListNode('ul');
const thirdLevelListNode = createListNode('ul');
const listItem1 = createListItemNode();
const listItem2 = createListItemNode();
const listItem3 = createListItemNode();
root.append(topListNode);
topListNode.append(listItem1);
listItem1.append(secondLevelListNode);
secondLevelListNode.append(listItem2);
listItem2.append(thirdLevelListNode);
thirdLevelListNode.append(listItem3);
const result = isLastItemInList(listItem3);
expect(result).toEqual(true);
});
});
test('isLastItemInList should return true if the listItem is the last in a non-nested list.', async () => {
const editor: OutlineEditor = testEnv.editor;
await editor.update((view: View) => {
// Root
// |- ListNode
// |- ListItemNode
// |- ListItemNode
const root = view.getRoot();
const topListNode = createListNode('ul');
const listItem1 = createListItemNode();
const listItem2 = createListItemNode();
root.append(topListNode);
topListNode.append(listItem1);
topListNode.append(listItem2);
const result = isLastItemInList(listItem2);
expect(result).toEqual(true);
});
});
test('isLastItemInList should return false if the listItem is not the last in a nested list.', async () => {
const editor: OutlineEditor = testEnv.editor;
await editor.update((view: View) => {
// Root
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
const root = view.getRoot();
const topListNode = createListNode('ul');
const secondLevelListNode = createListNode('ul');
const thirdLevelListNode = createListNode('ul');
const listItem1 = createListItemNode();
const listItem2 = createListItemNode();
const listItem3 = createListItemNode();
root.append(topListNode);
topListNode.append(listItem1);
listItem1.append(secondLevelListNode);
secondLevelListNode.append(listItem2);
listItem2.append(thirdLevelListNode);
thirdLevelListNode.append(listItem3);
const result = isLastItemInList(listItem2);
expect(result).toEqual(false);
});
});
test('isLastItemInList should return true if the listItem is not the last in a non-nested list.', async () => {
const editor: OutlineEditor = testEnv.editor;
await editor.update((view: View) => {
// Root
// |- ListNode
// |- ListItemNode
// |- ListItemNode
const root = view.getRoot();
const topListNode = createListNode('ul');
const listItem1 = createListItemNode();
const listItem2 = createListItemNode();
root.append(topListNode);
topListNode.append(listItem1);
topListNode.append(listItem2);
const result = isLastItemInList(listItem1);
expect(result).toEqual(false);
});
});
});
});

View File

@ -42,5 +42,6 @@
"40": "createNode: node does not exist in nodeMap",
"41": "reconcileNode: prevNode or nextNode does not exist in nodeMap",
"42": "reconcileNode: parentDOM is null",
"43": "Reconciliation: could not find DOM element for node key \"${key}\""
"43": "Reconciliation: could not find DOM element for node key \"${key}\"",
"44": "A ListItemNode must have a ListNode for a parent."
}