mirror of
https://github.com/facebook/lexical.git
synced 2025-08-06 16:39:33 +08:00
Move list feature into a separate package (#1239)
* wip * fix build issues * fix imports * bump version
This commit is contained in:
@ -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'
|
||||
|
@ -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
17
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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';
|
||||
|
@ -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>,
|
||||
|
@ -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) => {
|
||||
|
@ -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', () => {
|
||||
|
3
packages/lexical-list/LexicalList.js
Normal file
3
packages/lexical-list/LexicalList.js
Normal file
@ -0,0 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = require('./dist/LexicalList.js');
|
3
packages/lexical-list/README.md
Normal file
3
packages/lexical-list/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Lexical Lists
|
||||
|
||||
This package contains the fucntinality for the List feature of Lexical.
|
25
packages/lexical-list/package.json
Normal file
25
packages/lexical-list/package.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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,
|
@ -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';
|
||||
|
@ -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: {
|
@ -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: {
|
272
packages/lexical-list/src/__tests__/unit/utils.test.js
Normal file
272
packages/lexical-list/src/__tests__/unit/utils.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
322
packages/lexical-list/src/formatList.js
Normal file
322
packages/lexical-list/src/formatList.js
Normal 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');
|
||||
}
|
29
packages/lexical-list/src/index.js
Normal file
29
packages/lexical-list/src/index.js
Normal 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,
|
||||
};
|
113
packages/lexical-list/src/utils.js
Normal file
113
packages/lexical-list/src/utils.js
Normal 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);
|
||||
}
|
@ -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',
|
||||
|
@ -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",
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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';
|
||||
|
@ -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}`,
|
||||
|
Reference in New Issue
Block a user