Move list feature into a separate package (#1239)

* wip

* fix build issues

* fix imports

* bump version
This commit is contained in:
Acy Watson
2022-02-08 12:33:10 -08:00
committed by acywatson
parent 73522824b6
commit 2020bbadd5
28 changed files with 827 additions and 705 deletions

View File

@ -18,8 +18,6 @@ server.max_workers=4
module.name_mapper='^lexical$' -> '<PROJECT_ROOT>/packages/lexical/src/index.js'
module.name_mapper='^lexical/HeadingNode' -> '<PROJECT_ROOT>/packages/lexical/src/nodes/extended/LexicalHeadingNode.js'
module.name_mapper='^lexical/ListNode' -> '<PROJECT_ROOT>/packages/lexical/src/nodes/extended/LexicalListNode.js'
module.name_mapper='^lexical/ListItemNode' -> '<PROJECT_ROOT>/packages/lexical/src/nodes/extended/LexicalListItemNode.js'
module.name_mapper='^lexical/TableNode' -> '<PROJECT_ROOT>/packages/lexical/src/nodes/extended/LexicalTableNode.js'
module.name_mapper='^lexical/TableRowNode' -> '<PROJECT_ROOT>/packages/lexical/src/nodes/extended/LexicalTableRowNode.js'
module.name_mapper='^lexical/TableCellNode' -> '<PROJECT_ROOT>/packages/lexical/src/nodes/extended/LexicalTableCellNode.js'
@ -30,6 +28,8 @@ module.name_mapper='^lexical/AutoLinkNode' -> '<PROJECT_ROOT>/packages/lexical/s
module.name_mapper='^lexical/HashtagNode' -> '<PROJECT_ROOT>/packages/lexical/src/nodes/extended/LexicalHashtagNode.js'
module.name_mapper='^lexical/CodeHighlightNode' -> '<PROJECT_ROOT>/packages/lexical/src/nodes/extended/LexicalCodeHighlightNode.js'
module.name_mapper='^@lexical/list' -> '<PROJECT_ROOT>/packages/lexical-list/src/index.js'
module.name_mapper='^@lexical/helpers/selection' -> '<PROJECT_ROOT>/packages/lexical-helpers/src/LexicalSelectionHelpers.js'
module.name_mapper='^@lexical/helpers/text' -> '<PROJECT_ROOT>/packages/lexical-helpers/src/LexicalTextHelpers.js'
module.name_mapper='^@lexical/helpers/nodes' -> '<PROJECT_ROOT>/packages/lexical-helpers/src/LexicalNodeHelpers.js'

View File

@ -21,10 +21,6 @@ module.exports = {
'^lexical$': '<rootDir>/packages/lexical/src/index.js',
'^lexical/HeadingNode$':
'<rootDir>/packages/lexical/src/nodes/extended/LexicalHeadingNode.js',
'^lexical/ListNode$':
'<rootDir>/packages/lexical/src/nodes/extended/LexicalListNode.js',
'^lexical/ListItemNode$':
'<rootDir>/packages/lexical/src/nodes/extended/LexicalListItemNode.js',
'^lexical/TableNode$':
'<rootDir>/packages/lexical/src/nodes/extended/LexicalTableNode.js',
'^lexical/TableRowNode$':
@ -43,6 +39,7 @@ module.exports = {
'<rootDir>/packages/lexical/src/nodes/extended/LexicalHashtagNode.js',
'^lexical/CodeHighlightNode$':
'<rootDir>/packages/lexical/src/nodes/extended/LexicalCodeHighlightNode.js',
'^@lexical/list$': '<rootDir>/packages/lexical-list/src/index.js',
'^@lexical/react/DEPRECATED_useLexicalRichText$':
'<rootDir>/packages/lexical-react/src/DEPRECATED_useLexicalRichText.js',
'^@lexical/react/useLexicalCanShowPlaceholder$':

17
package-lock.json generated
View File

@ -6874,6 +6874,10 @@
"resolved": "packages/lexical-helpers",
"link": true
},
"node_modules/@lexical/list": {
"resolved": "packages/lexical-list",
"link": true
},
"node_modules/@lexical/react": {
"resolved": "packages/lexical-react",
"link": true
@ -32371,6 +32375,14 @@
},
"packages/lexical-helpers": {
"name": "@lexical/helpers",
"version": "0.1.5",
"license": "MIT",
"peerDependencies": {
"@lexical/list": "0.1.5",
"lexical": "0.1.5"
}
},
"packages/lexical-list": {
"version": "0.1.5",
"license": "MIT",
"peerDependencies": {
@ -32383,6 +32395,7 @@
"@craco/craco": "6.1.2",
"@excalidraw/excalidraw": "latest",
"@lexical/helpers": "0.1.5",
"@lexical/list": "0.1.5",
"@lexical/react": "0.1.5",
"lexical": "0.1.5",
"link-preview-generator": "1.0.7",
@ -36851,6 +36864,9 @@
"@lexical/helpers": {
"version": "file:packages/lexical-helpers"
},
"@lexical/list": {
"version": "file:packages/lexical-list"
},
"@lexical/react": {
"version": "file:packages/lexical-react"
},
@ -45462,6 +45478,7 @@
"@craco/craco": "6.1.2",
"@excalidraw/excalidraw": "latest",
"@lexical/helpers": "0.1.5",
"@lexical/list": "0.1.5",
"@lexical/react": "0.1.5",
"babel-plugin-transform-stylex": "1.0.0",
"lexical": "0.1.5",

View File

@ -15,7 +15,8 @@
"license": "MIT",
"version": "0.1.6",
"peerDependencies": {
"lexical": "0.1.6"
"lexical": "0.1.6",
"@lexical/list": "0.1.6"
},
"repository": {
"type": "git",

View File

@ -29,8 +29,7 @@ import {
$createParagraphNode,
} from 'lexical';
import getPossibleDecoratorNode from 'shared/getPossibleDecoratorNode';
import {$createListNode} from 'lexical/ListNode';
import {$createListItemNode} from 'lexical/ListItemNode';
import {$createListNode, $createListItemNode} from '@lexical/list';
import {$createHeadingNode} from 'lexical/HeadingNode';
import {$createLinkNode} from 'lexical/LinkNode';
import {$createCodeNode} from 'lexical/CodeNode';

View File

@ -7,14 +7,9 @@
* @flow strict
*/
import type {ListItemNode} from 'lexical/ListItemNode';
import type {LexicalNode} from 'lexical';
import type {ListNode} from 'lexical/ListNode';
import type {TableNode} from 'lexical/TableNode';
import {$isListNode} from 'lexical/ListNode';
import {$isListItemNode} from 'lexical/ListItemNode';
import invariant from 'shared/invariant';
import {
$isElementNode,
$createTextNode,
@ -54,76 +49,6 @@ export function $dfs__DEPRECATED(
}
}
export function $getListDepth(listNode: ListNode): number {
let depth = 1;
let parent = listNode.getParent();
while (parent != null) {
if ($isListItemNode(parent)) {
const parentList = parent.getParent();
if ($isListNode(parentList)) {
depth++;
parent = parentList.getParent();
continue;
}
invariant(false, 'A ListItemNode must have a ListNode for a parent.');
}
return depth;
}
return depth;
}
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;
}
// This should probably be $getAllChildrenOfType
export function $getAllListItems(node: ListNode): Array<ListItemNode> {
let listItemNodes: Array<ListItemNode> = [];
//$FlowFixMe - the result of this will always be an array of ListItemNodes.
const listChildren: Array<ListItemNode> = node
.getChildren()
.filter($isListItemNode);
for (let i = 0; i < listChildren.length; i++) {
const listItemNode = listChildren[i];
const firstChild = listItemNode.getFirstChild();
if ($isListNode(firstChild)) {
listItemNodes = listItemNodes.concat($getAllListItems(firstChild));
} else {
listItemNodes.push(listItemNode);
}
}
return listItemNodes;
}
export function $getNearestNodeOfType<T: LexicalNode>(
node: LexicalNode,
klass: Class<T>,

View File

@ -15,9 +15,6 @@ import {
} from '../../../../lexical/src/__tests__/utils';
import {
$dfs__DEPRECATED,
$getListDepth,
$getTopListNode,
$isLastItemInList,
$areSiblingsNullOrSpace,
$getNearestNodeOfType,
} from '@lexical/helpers/nodes';
@ -27,8 +24,7 @@ import {
$createParagraphNode,
$isParagraphNode,
} from 'lexical';
import {$createListNode, ListNode} from 'lexical/ListNode';
import {$createListItemNode} from 'lexical/ListItemNode';
import {$createListNode, $createListItemNode, ListNode} from '@lexical/list';
describe('LexicalNodeHelpers tests', () => {
initializeUnitTest((testEnv) => {
@ -124,262 +120,6 @@ describe('LexicalNodeHelpers tests', () => {
expect(dfsKeys).toEqual(expectedKeys);
});
test('getListDepth should return the 1-based depth of a list with one levels', async () => {
const editor: LexicalEditor = testEnv.editor;
await editor.update((state: State) => {
// Root
// |- ListNode
const root = $getRoot();
const topListNode = $createListNode('ul');
root.append(topListNode);
const result = $getListDepth(topListNode);
expect(result).toEqual(1);
});
});
test('getListDepth should return the 1-based depth of a list with two levels', async () => {
const editor: LexicalEditor = testEnv.editor;
await editor.update((state: State) => {
// Root
// |- ListNode
// |- ListItemNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
const root = $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 = $getListDepth(secondLevelListNode);
expect(result).toEqual(2);
});
});
test('getListDepth should return the 1-based depth of a list with five levels', async () => {
const editor: LexicalEditor = testEnv.editor;
await editor.update((state: State) => {
// Root
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
// |- ListNode
const root = $getRoot();
const topListNode = $createListNode('ul');
const listNode2 = $createListNode('ul');
const listNode3 = $createListNode('ul');
const listNode4 = $createListNode('ul');
const listNode5 = $createListNode('ul');
const listItem1 = $createListItemNode();
const listItem2 = $createListItemNode();
const listItem3 = $createListItemNode();
const listItem4 = $createListItemNode();
root.append(topListNode);
topListNode.append(listItem1);
listItem1.append(listNode2);
listNode2.append(listItem2);
listItem2.append(listNode3);
listNode3.append(listItem3);
listItem3.append(listNode4);
listNode4.append(listItem4);
listItem4.append(listNode5);
const result = $getListDepth(listNode5);
expect(result).toEqual(5);
});
});
test('getTopListNode should return the top list node when the list is a direct child of the RootNode', async () => {
const editor: LexicalEditor = testEnv.editor;
await editor.update((state: State) => {
// Root
// |- ListNode
// |- ListItemNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
const root = $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: LexicalEditor = testEnv.editor;
await editor.update((state: State) => {
// Root
// |- ParagaphNode
// |- ListNode
// |- ListItemNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
const root = $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: LexicalEditor = testEnv.editor;
await editor.update((state: State) => {
// Root
// |- ParagaphNode
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
// |- ListItemNode
const root = $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: LexicalEditor = testEnv.editor;
await editor.update((state: State) => {
// Root
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
const root = $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: LexicalEditor = testEnv.editor;
await editor.update((state: State) => {
// Root
// |- ListNode
// |- ListItemNode
// |- ListItemNode
const root = $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: LexicalEditor = testEnv.editor;
await editor.update((state: State) => {
// Root
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
const root = $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: LexicalEditor = testEnv.editor;
await editor.update((state: State) => {
// Root
// |- ListNode
// |- ListItemNode
// |- ListItemNode
const root = $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);
});
});
test('getNearestNodeOfType should return the top node if it is of the given type.', async () => {
const editor: LexicalEditor = testEnv.editor;
await editor.update((state: State) => {

View File

@ -23,8 +23,7 @@ import {
$createTestDecoratorNode,
createTestEditor,
} from '../../../../lexical/src/__tests__/utils';
import {$createListNode} from 'lexical/ListNode';
import {$createListItemNode} from 'lexical/ListItemNode';
import {$createListNode, $createListItemNode} from '@lexical/list';
import {$createLinkNode} from 'lexical/LinkNode';
jest.mock('shared/environment', () => {

View File

@ -0,0 +1,3 @@
'use strict';
module.exports = require('./dist/LexicalList.js');

View File

@ -0,0 +1,3 @@
# Lexical Lists
This package contains the fucntinality for the List feature of Lexical.

View File

@ -0,0 +1,25 @@
{
"name": "@lexical/list",
"author": {
"name": "Dominic Gannaway",
"email": "dg@domgan.com"
},
"description": "This package provides the list feature for Lexical.",
"keywords": [
"lexical",
"editor",
"rich-text",
"list"
],
"license": "MIT",
"version": "0.1.6",
"main": "LexicalList.js",
"peerDependencies": {
"lexical": "0.1.6"
},
"repository": {
"type": "git",
"url": "https://github.com/facebook/lexical",
"directory": "packages/lexical-list"
}
}

View File

@ -22,9 +22,9 @@ import {
$createParagraphNode,
$isParagraphNode,
} from 'lexical';
import {$createListNode, $isListNode} from 'lexical/ListNode';
import {$createListNode, $isListNode} from '@lexical/list';
import invariant from 'shared/invariant';
import {$getTopListNode, $isLastItemInList} from '@lexical/helpers/nodes';
import {$getTopListNode, $isLastItemInList} from './utils';
import {
addClassNamesToElement,
removeClassNamesFromElement,

View File

@ -15,13 +15,13 @@ import type {
} from 'lexical';
import {$createTextNode, ElementNode} from 'lexical';
import {$createListItemNode, $isListItemNode} from 'lexical/ListItemNode';
import {$createListItemNode, $isListItemNode} from '@lexical/list';
import {
addClassNamesToElement,
removeClassNamesFromElement,
} from '@lexical/helpers/elements';
import {$getListDepth} from '@lexical/helpers/nodes';
import {$getListDepth} from './utils';
type ListNodeTagType = 'ul' | 'ol';

View File

@ -6,14 +6,14 @@
*
*/
import {ListNode} from 'lexical/ListNode';
import {
ListNode,
ListItemNode,
$createListItemNode,
$isListItemNode,
} from 'lexical/ListItemNode';
} from '@lexical/list';
import {TextNode, $getRoot} from 'lexical';
import {initializeUnitTest} from '../../../../../../lexical/src/__tests__/utils';
import {initializeUnitTest} from '../../../../lexical/src/__tests__/utils';
const editorConfig = Object.freeze({
theme: {

View File

@ -6,15 +6,16 @@
*
*/
import {$createListNode, $isListNode} from 'lexical/ListNode';
import {initializeUnitTest} from '../../../../../../lexical/src/__tests__/utils';
import {
$createListNode,
ListNode,
$isListNode,
$createListItemNode,
$isListItemNode,
ListItemNode,
} from 'lexical/ListItemNode';
} from '@lexical/list';
import {initializeUnitTest} from '../../../../lexical/src/__tests__/utils';
import {TextNode, ParagraphNode} from 'lexical';
import {ListNode} from '../../LexicalListNode';
const editorConfig = Object.freeze({
theme: {

View File

@ -0,0 +1,272 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {initializeUnitTest} from '../../../../lexical/src/__tests__/utils';
import {$getRoot, $createParagraphNode} from 'lexical';
import {$createListNode, $createListItemNode} from '@lexical/list';
import {$getListDepth, $getTopListNode, $isLastItemInList} from '../../utils';
describe('Lexical List Utils tests', () => {
initializeUnitTest((testEnv) => {
test('getListDepth should return the 1-based depth of a list with one levels', async () => {
const editor: LexicalEditor = testEnv.editor;
await editor.update((state: State) => {
// Root
// |- ListNode
const root = $getRoot();
const topListNode = $createListNode('ul');
root.append(topListNode);
const result = $getListDepth(topListNode);
expect(result).toEqual(1);
});
});
test('getListDepth should return the 1-based depth of a list with two levels', async () => {
const editor: LexicalEditor = testEnv.editor;
await editor.update((state: State) => {
// Root
// |- ListNode
// |- ListItemNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
const root = $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 = $getListDepth(secondLevelListNode);
expect(result).toEqual(2);
});
});
test('getListDepth should return the 1-based depth of a list with five levels', async () => {
const editor: LexicalEditor = testEnv.editor;
await editor.update((state: State) => {
// Root
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
// |- ListNode
const root = $getRoot();
const topListNode = $createListNode('ul');
const listNode2 = $createListNode('ul');
const listNode3 = $createListNode('ul');
const listNode4 = $createListNode('ul');
const listNode5 = $createListNode('ul');
const listItem1 = $createListItemNode();
const listItem2 = $createListItemNode();
const listItem3 = $createListItemNode();
const listItem4 = $createListItemNode();
root.append(topListNode);
topListNode.append(listItem1);
listItem1.append(listNode2);
listNode2.append(listItem2);
listItem2.append(listNode3);
listNode3.append(listItem3);
listItem3.append(listNode4);
listNode4.append(listItem4);
listItem4.append(listNode5);
const result = $getListDepth(listNode5);
expect(result).toEqual(5);
});
});
test('getTopListNode should return the top list node when the list is a direct child of the RootNode', async () => {
const editor: LexicalEditor = testEnv.editor;
await editor.update((state: State) => {
// Root
// |- ListNode
// |- ListItemNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
const root = $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: LexicalEditor = testEnv.editor;
await editor.update((state: State) => {
// Root
// |- ParagaphNode
// |- ListNode
// |- ListItemNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
const root = $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: LexicalEditor = testEnv.editor;
await editor.update((state: State) => {
// Root
// |- ParagaphNode
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
// |- ListItemNode
const root = $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: LexicalEditor = testEnv.editor;
await editor.update((state: State) => {
// Root
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
const root = $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: LexicalEditor = testEnv.editor;
await editor.update((state: State) => {
// Root
// |- ListNode
// |- ListItemNode
// |- ListItemNode
const root = $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: LexicalEditor = testEnv.editor;
await editor.update((state: State) => {
// Root
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
// |- ListNode
// |- ListItemNode
const root = $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: LexicalEditor = testEnv.editor;
await editor.update((state: State) => {
// Root
// |- ListNode
// |- ListItemNode
// |- ListItemNode
const root = $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

@ -0,0 +1,322 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
*/
import type {LexicalEditor, ElementNode} from 'lexical';
import type {ListNode} from '@lexical/list';
import {
$getSelection,
$log,
$isLeafNode,
$isRootNode,
$isElementNode,
$createParagraphNode
} from 'lexical';
import {
$createListItemNode,
$isListItemNode,
ListItemNode,
$createListNode,
$isListNode,
} from '@lexical/list';
import {
$getAllListItems,
$getTopListNode,
getUniqueListItemNodes,
findNearestListItemNode,
isNestedListNode,
} from './utils';
import {$getNearestNodeOfType} from '@lexical/helpers/nodes';
export function insertList(editor: LexicalEditor, listType: 'ul' | 'ol'): void {
editor.update(() => {
$log('formatList');
const selection = $getSelection();
if (selection !== null) {
const nodes = selection.getNodes();
const anchor = selection.anchor;
const anchorNode = anchor.getNode();
const anchorNodeParent = anchorNode.getParent();
// This is a special case for when there's nothing selected
if (nodes.length === 0) {
const list = $createListNode(listType);
if ($isRootNode(anchorNodeParent)) {
anchorNode.replace(list);
const listItem = $createListItemNode();
list.append(listItem);
} else if ($isListItemNode(anchorNode)) {
const parent = anchorNode.getParentOrThrow();
list.append(...parent.getChildren());
parent.replace(list);
}
return;
} else {
const handled = new Set();
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (
$isElementNode(node) &&
node.isEmpty() &&
!handled.has(node.getKey())
) {
createListOrMerge(node, listType);
continue;
}
if ($isLeafNode(node)) {
let parent = node.getParent();
while (parent != null) {
const parentKey = parent.getKey();
if ($isListNode(parent)) {
if (!handled.has(parentKey)) {
const newListNode = $createListNode(listType);
newListNode.append(...parent.getChildren());
parent.replace(newListNode);
handled.add(parentKey);
}
break;
} else {
const nextParent = parent.getParent();
if ($isRootNode(nextParent) && !handled.has(parentKey)) {
handled.add(parentKey);
createListOrMerge(parent, listType);
break;
}
parent = nextParent;
}
}
}
}
}
}
});
}
function createListOrMerge(node: ElementNode, listType: 'ul' | 'ol'): ListNode {
if ($isListNode(node)) {
return node;
}
const previousSibling = node.getPreviousSibling();
const nextSibling = node.getNextSibling();
const listItem = $createListItemNode();
if ($isListNode(previousSibling) && listType === previousSibling.getTag()) {
listItem.append(node);
previousSibling.append(listItem);
// if the same type of list is on both sides, merge them.
if ($isListNode(nextSibling) && listType === nextSibling.getTag()) {
previousSibling.append(...nextSibling.getChildren());
nextSibling.remove();
}
return previousSibling;
} else if ($isListNode(nextSibling) && listType === nextSibling.getTag()) {
listItem.append(node);
nextSibling.getFirstChildOrThrow().insertBefore(listItem);
return nextSibling;
} else {
const list = $createListNode(listType);
list.append(listItem);
node.replace(list);
listItem.append(node);
return list;
}
}
export function removeList(editor: LexicalEditor): void {
editor.update(() => {
$log('removeList');
const selection = $getSelection();
if (selection !== null) {
const listNodes = new Set();
const nodes = selection.getNodes();
const anchorNode = selection.anchor.getNode();
if (nodes.length === 0 && $isListItemNode(anchorNode)) {
listNodes.add($getTopListNode(anchorNode));
} else {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if ($isLeafNode(node)) {
const listItemNode = $getNearestNodeOfType(node, ListItemNode);
if (listItemNode != null) {
listNodes.add($getTopListNode(listItemNode));
}
}
}
}
listNodes.forEach((listNode) => {
let insertionPoint = listNode;
const listItems = $getAllListItems(listNode);
listItems.forEach((listItemNode) => {
if (listItemNode != null) {
const paragraph = $createParagraphNode();
paragraph.append(...listItemNode.getChildren());
insertionPoint.insertAfter(paragraph);
insertionPoint = paragraph;
listItemNode.remove();
}
});
listNode.remove();
});
}
});
}
function handleIndent(listItemNodes: Array<ListItemNode>): void {
// go through each node and decide where to move it.
listItemNodes.forEach((listItemNode) => {
if (isNestedListNode(listItemNode)) {
return;
}
const parent = listItemNode.getParent();
const nextSibling = listItemNode.getNextSibling();
const previousSibling = listItemNode.getPreviousSibling();
// if there are nested lists on either side, merge them all together.
if (isNestedListNode(nextSibling) && isNestedListNode(previousSibling)) {
const innerList = previousSibling.getFirstChild();
if ($isListNode(innerList)) {
innerList.append(listItemNode);
const nextInnerList = nextSibling.getFirstChild();
if ($isListNode(nextInnerList)) {
const children = nextInnerList.getChildren();
innerList.append(...children);
nextInnerList.remove();
}
innerList.getChildren().forEach((child) => child.markDirty());
}
} else if (isNestedListNode(nextSibling)) {
// if the ListItemNode is next to a nested ListNode, merge them
const innerList = nextSibling.getFirstChild();
if ($isListNode(innerList)) {
const firstChild = innerList.getFirstChild();
if (firstChild !== null) {
firstChild.insertBefore(listItemNode);
}
innerList.getChildren().forEach((child) => child.markDirty());
}
} else if (isNestedListNode(previousSibling)) {
const innerList = previousSibling.getFirstChild();
if ($isListNode(innerList)) {
innerList.append(listItemNode);
innerList.getChildren().forEach((child) => child.markDirty());
}
} else {
// otherwise, we need to create a new nested ListNode
if ($isListNode(parent)) {
const newListItem = $createListItemNode();
const newList = $createListNode(parent.getTag());
newListItem.append(newList);
newList.append(listItemNode);
if (previousSibling) {
previousSibling.insertAfter(newListItem);
} else if (nextSibling) {
nextSibling.insertBefore(newListItem);
} else {
parent.append(newListItem);
}
}
}
if ($isListNode(parent)) {
parent.getChildren().forEach((child) => child.markDirty());
}
});
}
function handleOutdent(listItemNodes: Array<ListItemNode>): void {
// go through each node and decide where to move it.
listItemNodes.forEach((listItemNode) => {
if (isNestedListNode(listItemNode)) {
return;
}
const parentList = listItemNode.getParent();
const grandparentListItem = parentList ? parentList.getParent() : undefined;
const greatGrandparentList = grandparentListItem
? grandparentListItem.getParent()
: undefined;
// If it doesn't have these ancestors, it's not indented.
if (
$isListNode(greatGrandparentList) &&
$isListItemNode(grandparentListItem) &&
$isListNode(parentList)
) {
// if it's the first child in it's parent list, insert it into the
// great grandparent list before the grandparent
const firstChild = parentList ? parentList.getFirstChild() : undefined;
const lastChild = parentList ? parentList.getLastChild() : undefined;
if (listItemNode.is(firstChild)) {
grandparentListItem.insertBefore(listItemNode);
if (parentList.isEmpty()) {
grandparentListItem.remove();
}
// if it's the last child in it's parent list, insert it into the
// great grandparent list after the grandparent.
} else if (listItemNode.is(lastChild)) {
grandparentListItem.insertAfter(listItemNode);
if (parentList.isEmpty()) {
grandparentListItem.remove();
}
} else {
// otherwise, we need to split the siblings into two new nested lists
const tag = parentList.getTag();
const previousSiblingsListItem = $createListItemNode();
const previousSiblingsList = $createListNode(tag);
previousSiblingsListItem.append(previousSiblingsList);
listItemNode
.getPreviousSiblings()
.forEach((sibling) => previousSiblingsList.append(sibling));
const nextSiblingsListItem = $createListItemNode();
const nextSiblingsList = $createListNode(tag);
nextSiblingsListItem.append(nextSiblingsList);
nextSiblingsList.append(...listItemNode.getNextSiblings());
// put the sibling nested lists on either side of the grandparent list item in the great grandparent.
grandparentListItem.insertBefore(previousSiblingsListItem);
grandparentListItem.insertAfter(nextSiblingsListItem);
// replace the grandparent list item (now between the siblings) with the outdented list item.
grandparentListItem.replace(listItemNode);
}
parentList.getChildren().forEach((child) => child.markDirty());
greatGrandparentList.getChildren().forEach((child) => child.markDirty());
}
});
}
function maybeIndentOrOutdent(direction: 'indent' | 'outdent'): boolean {
const selection = $getSelection();
if (selection === null) {
return false;
}
const selectedNodes = selection.getNodes();
let listItemNodes = [];
if (selectedNodes.length === 0) {
selectedNodes.push(selection.anchor.getNode());
}
if (selectedNodes.length === 1) {
// Only 1 node selected. Selection may not contain the ListNodeItem so we traverse the tree to
// find whether this is part of a ListItemNode
const nearestListItemNode = findNearestListItemNode(selectedNodes[0]);
if (nearestListItemNode !== null) {
listItemNodes = [nearestListItemNode];
}
} else {
listItemNodes = getUniqueListItemNodes(selectedNodes);
}
if (listItemNodes.length > 0) {
if (direction === 'indent') {
handleIndent(listItemNodes);
} else {
handleOutdent(listItemNodes);
}
return true;
}
return false;
}
export function indentList(): boolean {
return maybeIndentOrOutdent('indent');
}
export function outdentList(): boolean {
return maybeIndentOrOutdent('outdent');
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
*/
import {ListNode, $createListNode, $isListNode} from './LexicalListNode';
import {
ListItemNode,
$createListItemNode,
$isListItemNode,
} from './LexicalListItemNode';
import {insertList, removeList, indentList, outdentList} from './formatList';
export {
ListNode,
$createListNode,
$isListNode,
ListItemNode,
$createListItemNode,
$isListItemNode,
insertList,
removeList,
indentList,
outdentList,
};

View File

@ -0,0 +1,113 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
*/
import type {LexicalNode} from 'lexical';
import type {ListNode} from '@lexical/list';
import {ListItemNode, $isListNode, $isListItemNode} from '@lexical/list';
import invariant from 'shared/invariant';
export function $getListDepth(listNode: ListNode): number {
let depth = 1;
let parent = listNode.getParent();
while (parent != null) {
if ($isListItemNode(parent)) {
const parentList = parent.getParent();
if ($isListNode(parentList)) {
depth++;
parent = parentList.getParent();
continue;
}
invariant(false, 'A ListItemNode must have a ListNode for a parent.');
}
return depth;
}
return depth;
}
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;
}
// This should probably be $getAllChildrenOfType
export function $getAllListItems(node: ListNode): Array<ListItemNode> {
let listItemNodes: Array<ListItemNode> = [];
//$FlowFixMe - the result of this will always be an array of ListItemNodes.
const listChildren: Array<ListItemNode> = node
.getChildren()
.filter($isListItemNode);
for (let i = 0; i < listChildren.length; i++) {
const listItemNode = listChildren[i];
const firstChild = listItemNode.getFirstChild();
if ($isListNode(firstChild)) {
listItemNodes = listItemNodes.concat($getAllListItems(firstChild));
} else {
listItemNodes.push(listItemNode);
}
}
return listItemNodes;
}
export function isNestedListNode(node: ?LexicalNode): boolean %checks {
return $isListItemNode(node) && $isListNode(node.getFirstChild());
}
export function findNearestListItemNode(
node: LexicalNode,
): ListItemNode | null {
let currentNode = node;
while (currentNode !== null) {
if ($isListItemNode(currentNode)) {
return currentNode;
}
currentNode = currentNode.getParent();
}
return null;
}
export function getUniqueListItemNodes(
nodeList: Array<LexicalNode>,
): Array<ListItemNode> {
const keys = new Set<ListItemNode>();
for (let i = 0; i < nodeList.length; i++) {
const node = nodeList[i];
if ($isListItemNode(node)) {
keys.add(node);
}
}
return Array.from(keys);
}

View File

@ -31,6 +31,9 @@ module.exports = {
'@lexical/helpers/offsets': '@lexical/helpers/dist/LexicalOffsetHelpers',
'@lexical/helpers/root': '@lexical/helpers/dist/LexicalRootHelpers',
// Lexical Features
'@lexical/list': '@lexical/list/dist/LexicalList.js',
// Lexical React
'@lexical/react/LexicalTreeView': '@lexical/react/dist/LexicalTreeView',
'@lexical/react/useLexicalEditor': '@lexical/react/dist/useLexicalEditor',

View File

@ -9,6 +9,7 @@
"lexical": "0.1.6",
"@lexical/react": "0.1.6",
"@lexical/helpers": "0.1.6",
"@lexical/list": "0.1.6",
"link-preview-generator": "1.0.7",
"@craco/craco": "6.1.2",
"@excalidraw/excalidraw": "latest",

View File

@ -21,7 +21,7 @@ import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {$isHeadingNode} from 'lexical/HeadingNode';
import {$createHeadingNode} from 'lexical/HeadingNode';
import {$isListNode, ListNode} from 'lexical/ListNode';
import {$isListNode, ListNode} from '@lexical/list';
import {$createQuoteNode} from 'lexical/QuoteNode';
import {$createCodeNode, $isCodeNode} from 'lexical/CodeNode';
import {$isHorizontalRuleNode} from 'lexical';

View File

@ -7,15 +7,13 @@
* @flow strict
*/
import type {ElementNode, TextNode} from 'lexical';
import type {LexicalNode, NodeKey} from 'lexical';
import type {ElementNode, TextNode, LexicalNode, NodeKey} from 'lexical';
import invariant from 'shared/invariant';
import {$isElementNode, $createParagraphNode} from 'lexical';
import {$createCodeNode} from 'lexical/CodeNode';
import {$createHeadingNode} from 'lexical/HeadingNode';
import {$createListItemNode} from 'lexical/ListItemNode';
import {$createListNode} from 'lexical/ListNode';
import {$createListItemNode, $createListNode} from '@lexical/list';
import {$createQuoteNode} from 'lexical/QuoteNode';
import {$joinTextNodesFromElementNode} from '@lexical/helpers/text';

View File

@ -17,7 +17,7 @@ import type {
TextNodeWithOffset,
} from './AutoFormatterUtils.js';
import {$isCodeNode} from 'lexical/CodeNode';
import {$isListItemNode} from 'lexical/ListItemNode';
import {$isListItemNode} from '@lexical/list';
import {$isElementNode, $isTextNode, $getSelection} from 'lexical';
import {useEffect} from 'react';
import {

View File

@ -7,357 +7,24 @@
* @flow strict
*/
import type {
LexicalEditor,
LexicalNode,
CommandListenerLowPriority,
ElementNode,
} from 'lexical';
import type {LexicalEditor, CommandListenerLowPriority} from 'lexical';
import {useEffect} from 'react';
import {
$getSelection,
$log,
$isLeafNode,
$isRootNode,
$isElementNode,
$createParagraphNode,
} from 'lexical';
import {
$createListItemNode,
$isListItemNode,
ListItemNode,
} from 'lexical/ListItemNode';
import {$createListNode, $isListNode} from 'lexical/ListNode';
import type {ListNode} from 'lexical/ListNode';
import {
$getAllListItems,
$getNearestNodeOfType,
$getTopListNode,
} from '@lexical/helpers/nodes';
import {insertList, removeList, indentList, outdentList} from '@lexical/list';
const LowPriority: CommandListenerLowPriority = 1;
function removeList(editor: LexicalEditor): void {
editor.update(() => {
$log('removeList');
const selection = $getSelection();
if (selection !== null) {
const listNodes = new Set();
const nodes = selection.getNodes();
const anchorNode = selection.anchor.getNode();
if (nodes.length === 0 && $isListItemNode(anchorNode)) {
listNodes.add($getTopListNode(anchorNode));
} else {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if ($isLeafNode(node)) {
const listItemNode = $getNearestNodeOfType(node, ListItemNode);
if (listItemNode != null) {
listNodes.add($getTopListNode(listItemNode));
}
}
}
}
listNodes.forEach((listNode) => {
let insertionPoint = listNode;
const listItems = $getAllListItems(listNode);
listItems.forEach((listItemNode) => {
if (listItemNode != null) {
const paragraph = $createParagraphNode();
paragraph.append(...listItemNode.getChildren());
insertionPoint.insertAfter(paragraph);
insertionPoint = paragraph;
listItemNode.remove();
}
});
listNode.remove();
});
}
});
}
function createListOrMerge(node: ElementNode, listType: 'ul' | 'ol'): ListNode {
if ($isListNode(node)) {
return node;
}
const previousSibling = node.getPreviousSibling();
const nextSibling = node.getNextSibling();
const listItem = $createListItemNode();
if ($isListNode(previousSibling) && listType === previousSibling.getTag()) {
listItem.append(node);
previousSibling.append(listItem);
// if the same type of list is on both sides, merge them.
if ($isListNode(nextSibling) && listType === nextSibling.getTag()) {
previousSibling.append(...nextSibling.getChildren());
nextSibling.remove();
}
return previousSibling;
} else if ($isListNode(nextSibling) && listType === nextSibling.getTag()) {
listItem.append(node);
nextSibling.getFirstChildOrThrow().insertBefore(listItem);
return nextSibling;
} else {
const list = $createListNode(listType);
list.append(listItem);
node.replace(list);
listItem.append(node);
return list;
}
}
function insertList(editor: LexicalEditor, listType: 'ul' | 'ol'): void {
editor.update(() => {
$log('formatList');
const selection = $getSelection();
if (selection !== null) {
const nodes = selection.getNodes();
const anchor = selection.anchor;
const anchorNode = anchor.getNode();
const anchorNodeParent = anchorNode.getParent();
// This is a special case for when there's nothing selected
if (nodes.length === 0) {
const list = $createListNode(listType);
if ($isRootNode(anchorNodeParent)) {
anchorNode.replace(list);
const listItem = $createListItemNode();
list.append(listItem);
} else if ($isListItemNode(anchorNode)) {
const parent = anchorNode.getParentOrThrow();
list.append(...parent.getChildren());
parent.replace(list);
}
return;
} else {
const handled = new Set();
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (
$isElementNode(node) &&
node.isEmpty() &&
!handled.has(node.getKey())
) {
createListOrMerge(node, listType);
continue;
}
if ($isLeafNode(node)) {
let parent = node.getParent();
while (parent != null) {
const parentKey = parent.getKey();
if ($isListNode(parent)) {
if (!handled.has(parentKey)) {
const newListNode = $createListNode(listType);
newListNode.append(...parent.getChildren());
parent.replace(newListNode);
handled.add(parentKey);
}
break;
} else {
const nextParent = parent.getParent();
if ($isRootNode(nextParent) && !handled.has(parentKey)) {
handled.add(parentKey);
createListOrMerge(parent, listType);
break;
}
parent = nextParent;
}
}
}
}
}
}
});
}
function maybeIndentOrOutdent(direction: 'indent' | 'outdent'): boolean {
const selection = $getSelection();
if (selection === null) {
return false;
}
const selectedNodes = selection.getNodes();
let listItemNodes = [];
if (selectedNodes.length === 0) {
selectedNodes.push(selection.anchor.getNode());
}
if (selectedNodes.length === 1) {
// Only 1 node selected. Selection may not contain the ListNodeItem so we traverse the tree to
// find whether this is part of a ListItemNode
const nearestListItemNode = findNearestListItemNode(selectedNodes[0]);
if (nearestListItemNode !== null) {
listItemNodes = [nearestListItemNode];
}
} else {
listItemNodes = getUniqueListItemNodes(selectedNodes);
}
if (listItemNodes.length > 0) {
if (direction === 'indent') {
handleIndent(listItemNodes);
} else {
handleOutdent(listItemNodes);
}
return true;
}
return false;
}
function isNestedListNode(node: ?LexicalNode): boolean %checks {
return $isListItemNode(node) && $isListNode(node.getFirstChild());
}
function findNearestListItemNode(node: LexicalNode): ListItemNode | null {
let currentNode = node;
while (currentNode !== null) {
if ($isListItemNode(currentNode)) {
return currentNode;
}
currentNode = currentNode.getParent();
}
return null;
}
function getUniqueListItemNodes(
nodeList: Array<LexicalNode>,
): Array<ListItemNode> {
const keys = new Set<ListItemNode>();
for (let i = 0; i < nodeList.length; i++) {
const node = nodeList[i];
if ($isListItemNode(node)) {
keys.add(node);
}
}
return Array.from(keys);
}
function handleIndent(listItemNodes: Array<ListItemNode>): void {
// go through each node and decide where to move it.
listItemNodes.forEach((listItemNode) => {
if (isNestedListNode(listItemNode)) {
return;
}
const parent = listItemNode.getParent();
const nextSibling = listItemNode.getNextSibling();
const previousSibling = listItemNode.getPreviousSibling();
// if there are nested lists on either side, merge them all together.
if (isNestedListNode(nextSibling) && isNestedListNode(previousSibling)) {
const innerList = previousSibling.getFirstChild();
if ($isListNode(innerList)) {
innerList.append(listItemNode);
const nextInnerList = nextSibling.getFirstChild();
if ($isListNode(nextInnerList)) {
const children = nextInnerList.getChildren();
innerList.append(...children);
nextInnerList.remove();
}
innerList.getChildren().forEach((child) => child.markDirty());
}
} else if (isNestedListNode(nextSibling)) {
// if the ListItemNode is next to a nested ListNode, merge them
const innerList = nextSibling.getFirstChild();
if ($isListNode(innerList)) {
const firstChild = innerList.getFirstChild();
if (firstChild !== null) {
firstChild.insertBefore(listItemNode);
}
innerList.getChildren().forEach((child) => child.markDirty());
}
} else if (isNestedListNode(previousSibling)) {
const innerList = previousSibling.getFirstChild();
if ($isListNode(innerList)) {
innerList.append(listItemNode);
innerList.getChildren().forEach((child) => child.markDirty());
}
} else {
// otherwise, we need to create a new nested ListNode
if ($isListNode(parent)) {
const newListItem = $createListItemNode();
const newList = $createListNode(parent.getTag());
newListItem.append(newList);
newList.append(listItemNode);
if (previousSibling) {
previousSibling.insertAfter(newListItem);
} else if (nextSibling) {
nextSibling.insertBefore(newListItem);
} else {
parent.append(newListItem);
}
}
}
if ($isListNode(parent)) {
parent.getChildren().forEach((child) => child.markDirty());
}
});
}
function handleOutdent(listItemNodes: Array<ListItemNode>): void {
// go through each node and decide where to move it.
listItemNodes.forEach((listItemNode) => {
if (isNestedListNode(listItemNode)) {
return;
}
const parentList = listItemNode.getParent();
const grandparentListItem = parentList ? parentList.getParent() : undefined;
const greatGrandparentList = grandparentListItem
? grandparentListItem.getParent()
: undefined;
// If it doesn't have these ancestors, it's not indented.
if (
$isListNode(greatGrandparentList) &&
$isListItemNode(grandparentListItem) &&
$isListNode(parentList)
) {
// if it's the first child in it's parent list, insert it into the
// great grandparent list before the grandparent
const firstChild = parentList ? parentList.getFirstChild() : undefined;
const lastChild = parentList ? parentList.getLastChild() : undefined;
if (listItemNode.is(firstChild)) {
grandparentListItem.insertBefore(listItemNode);
if (parentList.isEmpty()) {
grandparentListItem.remove();
}
// if it's the last child in it's parent list, insert it into the
// great grandparent list after the grandparent.
} else if (listItemNode.is(lastChild)) {
grandparentListItem.insertAfter(listItemNode);
if (parentList.isEmpty()) {
grandparentListItem.remove();
}
} else {
// otherwise, we need to split the siblings into two new nested lists
const tag = parentList.getTag();
const previousSiblingsListItem = $createListItemNode();
const previousSiblingsList = $createListNode(tag);
previousSiblingsListItem.append(previousSiblingsList);
listItemNode
.getPreviousSiblings()
.forEach((sibling) => previousSiblingsList.append(sibling));
const nextSiblingsListItem = $createListItemNode();
const nextSiblingsList = $createListNode(tag);
nextSiblingsListItem.append(nextSiblingsList);
nextSiblingsList.append(...listItemNode.getNextSiblings());
// put the sibling nested lists on either side of the grandparent list item in the great grandparent.
grandparentListItem.insertBefore(previousSiblingsListItem);
grandparentListItem.insertAfter(nextSiblingsListItem);
// replace the grandparent list item (now between the siblings) with the outdented list item.
grandparentListItem.replace(listItemNode);
}
parentList.getChildren().forEach((child) => child.markDirty());
greatGrandparentList.getChildren().forEach((child) => child.markDirty());
}
});
}
export default function useList(editor: LexicalEditor): void {
useEffect(() => {
return editor.addListener(
'command',
(type) => {
if (type === 'indentContent') {
const hasHandledIndention = maybeIndentOrOutdent('indent');
const hasHandledIndention = indentList();
if (hasHandledIndention) {
return true;
}
} else if (type === 'outdentContent') {
const hasHandledIndention = maybeIndentOrOutdent('outdent');
const hasHandledIndention = outdentList();
if (hasHandledIndention) {
return true;
}

View File

@ -23,10 +23,9 @@ import {
$createParagraphNode,
} from 'lexical';
import {HeadingNode} from 'lexical/HeadingNode';
import {ListNode} from 'lexical/ListNode';
import {ListNode, ListItemNode} from '@lexical/list';
import {QuoteNode} from 'lexical/QuoteNode';
import {CodeNode} from 'lexical/CodeNode';
import {ListItemNode} from 'lexical/ListItemNode';
import useLexicalDragonSupport from './useLexicalDragonSupport';
import {
onCutForRichText,

View File

@ -20,8 +20,7 @@ import {
ParagraphNode,
} from 'lexical';
import {HeadingNode} from 'lexical/HeadingNode';
import {ListNode} from 'lexical/ListNode';
import {ListItemNode} from 'lexical/ListItemNode';
import {ListNode, ListItemNode} from '@lexical/list';
import {LinkNode} from 'lexical/LinkNode';
import {QuoteNode} from 'lexical/QuoteNode';
import {CodeNode} from 'lexical/CodeNode';

View File

@ -41,11 +41,13 @@ if (isClean) {
fs.removeSync(path.resolve('./packages/lexical/dist'));
fs.removeSync(path.resolve('./packages/lexical-react/dist'));
fs.removeSync(path.resolve('./packages/lexical-helpers/dist'));
fs.removeSync(path.resolve('./packages/lexical-list/dist'));
fs.removeSync(path.resolve('./packages/lexical-yjs/dist'));
}
const wwwMappings = {
lexical: 'Lexical',
'lexical-list': 'LexicalList',
'react-dom': 'ReactDOMComet',
'@lexical/yjs': 'LexicalYjs',
};
@ -90,6 +92,7 @@ const externals = [
// modules that use Stylex to www (the babel plugin on www
// is different to that of the OSS version).
'lexical',
'@lexical/list',
'@lexical/yjs',
'react-dom',
'react',
@ -307,6 +310,12 @@ build(
path.resolve(`./packages/lexical/dist/${getFileName('Lexical')}`),
);
build(
'Lexical List',
path.resolve('./packages/lexical-list/src/index.js'),
path.resolve(`./packages/lexical-list/dist/${getFileName('LexicalList')}`),
);
lexicalNodes.forEach((module) => {
build(
`Lexical Core Nodes - ${module}`,