[lexical-react] Breaking change: Deprecate public default exports (#6088)

This commit is contained in:
Bob Ippolito
2024-05-16 14:40:34 -07:00
committed by GitHub
parent ca0a823a4e
commit 956ae8b55b
55 changed files with 450 additions and 329 deletions

View File

@ -111,6 +111,28 @@ module.exports = {
'lexical/no-optional-chaining': OFF,
},
},
{
files: [
'packages/*/src/index.ts',
'packages/*/src/index.tsx',
'packages/lexical-react/src/*.ts',
'packages/lexical-react/src/*.tsx',
],
rules: {
'no-restricted-exports': [
'error',
{
restrictDefaultExports: {
defaultFrom: true,
direct: true,
named: true,
namedFrom: true,
namespaceFrom: true,
},
},
],
},
},
],
parser: '@babel/eslint-parser',

View File

@ -69,6 +69,7 @@ module.name_mapper='^@lexical/react/LexicalPlainTextPlugin$' -> '<PROJECT_ROOT>/
module.name_mapper='^@lexical/react/LexicalRichTextPlugin$' -> '<PROJECT_ROOT>/packages/lexical-react/flow/LexicalRichTextPlugin.js.flow'
module.name_mapper='^@lexical/react/LexicalTabIndentationPlugin$' -> '<PROJECT_ROOT>/packages/lexical-react/flow/LexicalTabIndentationPlugin.js.flow'
module.name_mapper='^@lexical/react/LexicalTableOfContents$' -> '<PROJECT_ROOT>/packages/lexical-react/flow/LexicalTableOfContents.js.flow'
module.name_mapper='^@lexical/react/LexicalTableOfContentsPlugin$' -> '<PROJECT_ROOT>/packages/lexical-react/flow/LexicalTableOfContentsPlugin.js.flow'
module.name_mapper='^@lexical/react/LexicalTablePlugin$' -> '<PROJECT_ROOT>/packages/lexical-react/flow/LexicalTablePlugin.js.flow'
module.name_mapper='^@lexical/react/LexicalTreeView$' -> '<PROJECT_ROOT>/packages/lexical-react/flow/LexicalTreeView.js.flow'
module.name_mapper='^@lexical/react/LexicalTypeaheadMenuPlugin$' -> '<PROJECT_ROOT>/packages/lexical-react/flow/LexicalTypeaheadMenuPlugin.js.flow'

View File

@ -62,7 +62,7 @@ import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
const theme = {
// Theme styling goes here

View File

@ -24,7 +24,7 @@ module.exports = {
},
},
],
'@babel/preset-react',
['@babel/preset-react', {runtime: 'automatic'}],
'@babel/preset-flow',
],
};

View File

@ -7,7 +7,7 @@
*/
import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import ToolbarPlugin from './plugins/ToolbarPlugin';

View File

@ -8,7 +8,7 @@
import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin';
import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';

View File

@ -123,6 +123,9 @@
"@lexical/react/LexicalTableOfContents": [
"../lexical-react/src/LexicalTableOfContents.tsx"
],
"@lexical/react/LexicalTableOfContentsPlugin": [
"../lexical-react/src/LexicalTableOfContentsPlugin.tsx"
],
"@lexical/react/LexicalTablePlugin": [
"../lexical-react/src/LexicalTablePlugin.ts"
],

View File

@ -112,7 +112,7 @@ export default defineConfig({
},
],
],
presets: ['@babel/preset-react'],
presets: [['@babel/preset-react', {runtime: 'automatic'}]],
}),
react(),
],

View File

@ -14,4 +14,5 @@
import * as plugin from './LexicalEslintPlugin.js';
export type {RulesOfLexicalOptions} from './rules/rules-of-lexical.js';
// eslint-disable-next-line no-restricted-exports
export default plugin;

View File

@ -9,7 +9,7 @@
import {createEmptyHistoryState, registerHistory} from '@lexical/history';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {$createQuoteNode} from '@lexical/rich-text';

View File

@ -10,9 +10,9 @@ import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin';
import {CharacterLimitPlugin} from '@lexical/react/LexicalCharacterLimitPlugin';
import {CheckListPlugin} from '@lexical/react/LexicalCheckListPlugin';
import {ClearEditorPlugin} from '@lexical/react/LexicalClearEditorPlugin';
import LexicalClickableLinkPlugin from '@lexical/react/LexicalClickableLinkPlugin';
import {ClickableLinkPlugin} from '@lexical/react/LexicalClickableLinkPlugin';
import {CollaborationPlugin} from '@lexical/react/LexicalCollaborationPlugin';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {HashtagPlugin} from '@lexical/react/LexicalHashtagPlugin';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {HorizontalRulePlugin} from '@lexical/react/LexicalHorizontalRulePlugin';
@ -21,7 +21,7 @@ import {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {TabIndentationPlugin} from '@lexical/react/LexicalTabIndentationPlugin';
import {TablePlugin} from '@lexical/react/LexicalTablePlugin';
import useLexicalEditable from '@lexical/react/useLexicalEditable';
import {useLexicalEditable} from '@lexical/react/useLexicalEditable';
import * as React from 'react';
import {useEffect, useState} from 'react';
import {CAN_USE_DOM} from 'shared/canUseDOM';
@ -191,7 +191,7 @@ export default function Editor(): JSX.Element {
<TwitterPlugin />
<YouTubePlugin />
<FigmaPlugin />
<LexicalClickableLinkPlugin disabled={isEditable} />
<ClickableLinkPlugin disabled={isEditable} />
<HorizontalRulePlugin />
<EquationsPlugin />
<ExcalidrawPlugin />

View File

@ -19,7 +19,7 @@ import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin';
import {useCollaborationContext} from '@lexical/react/LexicalCollaborationContext';
import {CollaborationPlugin} from '@lexical/react/LexicalCollaborationPlugin';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {HashtagPlugin} from '@lexical/react/LexicalHashtagPlugin';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {LexicalNestedComposer} from '@lexical/react/LexicalNestedComposer';

View File

@ -12,7 +12,7 @@ import './InlineImageNode.css';
import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {LexicalNestedComposer} from '@lexical/react/LexicalNestedComposer';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {useLexicalNodeSelection} from '@lexical/react/useLexicalNodeSelection';

View File

@ -13,7 +13,7 @@ import './StickyNode.css';
import {useCollaborationContext} from '@lexical/react/LexicalCollaborationContext';
import {CollaborationPlugin} from '@lexical/react/LexicalCollaborationPlugin';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {LexicalNestedComposer} from '@lexical/react/LexicalNestedComposer';
import {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin';

View File

@ -32,7 +32,7 @@ import {useCollaborationContext} from '@lexical/react/LexicalCollaborationContex
import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {EditorRefPlugin} from '@lexical/react/LexicalEditorRefPlugin';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';
import {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin';

View File

@ -9,7 +9,7 @@
import type {ElementNode, LexicalEditor} from 'lexical';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import useLexicalEditable from '@lexical/react/useLexicalEditable';
import {useLexicalEditable} from '@lexical/react/useLexicalEditable';
import {
$deleteTableColumn__EXPERIMENTAL,
$deleteTableRow__EXPERIMENTAL,

View File

@ -11,7 +11,7 @@ import type {LexicalEditor} from 'lexical';
import './index.css';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import useLexicalEditable from '@lexical/react/useLexicalEditable';
import {useLexicalEditable} from '@lexical/react/useLexicalEditable';
import {
$getTableColumnIndexFromTableCellNode,
$getTableNodeFromLexicalNodeOrThrow,

View File

@ -5,14 +5,14 @@
* LICENSE file in the root directory of this source tree.
*
*/
import type {TableOfContentsEntry} from '@lexical/react/LexicalTableOfContents';
import type {TableOfContentsEntry} from '@lexical/react/LexicalTableOfContentsPlugin';
import type {HeadingTagType} from '@lexical/rich-text';
import type {NodeKey} from 'lexical';
import './index.css';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import LexicalTableOfContents from '@lexical/react/LexicalTableOfContents';
import {TableOfContentsPlugin as LexicalTableOfContentsPlugin} from '@lexical/react/LexicalTableOfContentsPlugin';
import {useEffect, useRef, useState} from 'react';
import * as React from 'react';
@ -188,10 +188,10 @@ function TableOfContentsList({
export default function TableOfContentsPlugin() {
return (
<LexicalTableOfContents>
<LexicalTableOfContentsPlugin>
{(tableOfContents) => {
return <TableOfContentsList tableOfContents={tableOfContents} />;
}}
</LexicalTableOfContents>
</LexicalTableOfContentsPlugin>
);
}

View File

@ -69,7 +69,7 @@ export default defineConfig(({command}) => {
},
],
],
presets: ['@babel/preset-react'],
presets: [['@babel/preset-react', {runtime: 'automatic'}]],
}),
react(),
viteCopyEsm(),

View File

@ -62,7 +62,7 @@ export default defineConfig({
exclude: '/**/node_modules/**',
extensions: ['jsx', 'js', 'ts', 'tsx', 'mjs'],
plugins: ['@babel/plugin-transform-flow-strip-types'],
presets: ['@babel/preset-react'],
presets: [['@babel/preset-react', {runtime: 'automatic'}]],
}),
react(),
viteCopyEsm(),

View File

@ -7,6 +7,8 @@
* @flow strict
*/
declare export function MLCClickableLinkPlugin({
declare export function ClickableLinkPlugin({
newTab?: boolean,
}): null;
declare export default typeof ClickableLinkPlugin;

View File

@ -12,6 +12,9 @@ export type LexicalErrorBoundaryProps = $ReadOnly<{
onError: (error: Error) => void,
}>;
declare export default function LexicalErrorBoundary(
declare export function LexicalErrorBoundary(
props: LexicalErrorBoundaryProps,
): React$Node;
/** @deprecated use the named export {@link LexicalErrorBoundary} */
export default typeof LexicalErrorBoundary;

View File

@ -9,7 +9,7 @@
import type {EditorState, LexicalEditor} from 'lexical';
import typeof LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import {typeof LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import * as React from 'react';

View File

@ -9,7 +9,7 @@
import type {EditorState, LexicalEditor} from 'lexical';
import typeof LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import {typeof LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import * as React from 'react';

View File

@ -7,11 +7,7 @@
* @flow strict
*/
import type {HeadingTagType} from '@lexical/rich-text';
import type {NodeKey} from 'lexical';
import {TableOfContentsPlugin} from '@lexical/react/LexicalTableOfContentsPlugin';
declare export default function LexicalTableOfContentsPlugin({
children: (
tableOfContents: Array<[NodeKey, string, HeadingTagType]>,
) => React$Node,
}): React$Node;
/** @deprecated use the named export {@link LexicalTableOfContentsPlugin} */
declare export default typeof TableOfContentsPlugin;

View File

@ -0,0 +1,18 @@
/**
* 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 {HeadingTagType} from '@lexical/rich-text';
import type {LexicalEditor, NodeKey} from 'lexical';
declare export function TableOfContentsPlugin({
children: (
tableOfContents: Array<[NodeKey, string, HeadingTagType]>,
editor: LexicalEditor,
) => React$Node,
}): React$Node;

View File

@ -9,4 +9,7 @@
import type {LexicalEditor} from 'lexical';
declare export default function useLexicalEditable(): boolean;
declare export function useLexicalEditable(): boolean;
/** @deprecated use the named export {@link useLexicalEditable} */
declare export default typeof useLexicalEditable;

View File

@ -14,6 +14,9 @@ export type LexicalSubscription<T> = {
subscribe: (callback: (value: T) => void) => () => void,
};
declare export default function useLexicalSubscription<T>(
declare export function useLexicalSubscription<T>(
subscription: (editor: LexicalEditor) => LexicalSubscription<T>,
): T;
/** @deprecated use the named export {@link useLexicalSubscription} */
declare export default typeof useLexicalSubscription;

View File

@ -1001,6 +1001,36 @@
"default": "./LexicalTableOfContents.js"
}
},
"./LexicalTableOfContentsPlugin": {
"import": {
"types": "./LexicalTableOfContentsPlugin.d.ts",
"development": "./LexicalTableOfContentsPlugin.dev.mjs",
"production": "./LexicalTableOfContentsPlugin.prod.mjs",
"node": "./LexicalTableOfContentsPlugin.node.mjs",
"default": "./LexicalTableOfContentsPlugin.mjs"
},
"require": {
"types": "./LexicalTableOfContentsPlugin.d.ts",
"development": "./LexicalTableOfContentsPlugin.dev.js",
"production": "./LexicalTableOfContentsPlugin.prod.js",
"default": "./LexicalTableOfContentsPlugin.js"
}
},
"./LexicalTableOfContentsPlugin.js": {
"import": {
"types": "./LexicalTableOfContentsPlugin.d.ts",
"development": "./LexicalTableOfContentsPlugin.dev.mjs",
"production": "./LexicalTableOfContentsPlugin.prod.mjs",
"node": "./LexicalTableOfContentsPlugin.node.mjs",
"default": "./LexicalTableOfContentsPlugin.mjs"
},
"require": {
"types": "./LexicalTableOfContentsPlugin.d.ts",
"development": "./LexicalTableOfContentsPlugin.dev.js",
"production": "./LexicalTableOfContentsPlugin.prod.js",
"default": "./LexicalTableOfContentsPlugin.js"
}
},
"./LexicalTablePlugin": {
"import": {
"types": "./LexicalTablePlugin.d.ts",

View File

@ -32,7 +32,7 @@ function findMatchingDOM<T extends Node>(
return null;
}
export default function LexicalClickableLinkPlugin({
export function ClickableLinkPlugin({
newTab = true,
disabled = false,
}: {
@ -122,3 +122,7 @@ export default function LexicalClickableLinkPlugin({
return null;
}
/** @deprecated use the named export {@link ClickableLinkPlugin} */
// eslint-disable-next-line no-restricted-exports
export default ClickableLinkPlugin;

View File

@ -6,7 +6,6 @@
*
*/
import * as React from 'react';
import {ErrorBoundary as ReactErrorBoundary} from 'react-error-boundary';
export type LexicalErrorBoundaryProps = {
@ -14,7 +13,7 @@ export type LexicalErrorBoundaryProps = {
onError: (error: Error) => void;
};
export default function LexicalErrorBoundary({
export function LexicalErrorBoundary({
children,
onError,
}: LexicalErrorBoundaryProps): JSX.Element {
@ -35,3 +34,7 @@ export default function LexicalErrorBoundary({
</ReactErrorBoundary>
);
}
/** @deprecated use the named export {@link LexicalErrorBoundary} */
// eslint-disable-next-line no-restricted-exports
export default LexicalErrorBoundary;

View File

@ -7,7 +7,7 @@
*/
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import useLexicalEditable from '@lexical/react/useLexicalEditable';
import {useLexicalEditable} from '@lexical/react/useLexicalEditable';
import * as React from 'react';
import {useCanShowPlaceholder} from './shared/useCanShowPlaceholder';

View File

@ -7,7 +7,7 @@
*/
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import useLexicalEditable from '@lexical/react/useLexicalEditable';
import {useLexicalEditable} from '@lexical/react/useLexicalEditable';
import * as React from 'react';
import {useCanShowPlaceholder} from './shared/useCanShowPlaceholder';

View File

@ -6,264 +6,15 @@
*
*/
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {$isHeadingNode, HeadingNode, HeadingTagType} from '@lexical/rich-text';
import {$getNextRightPreorderNode} from '@lexical/utils';
/** @deprecated moved to @lexical/react/LexicalTableOfContentsPlugin */
import {
$getNodeByKey,
$getRoot,
$isElementNode,
ElementNode,
LexicalEditor,
NodeKey,
NodeMutation,
TextNode,
} from 'lexical';
import {useEffect, useState} from 'react';
type TableOfContentsEntry,
TableOfContentsPlugin,
} from './LexicalTableOfContentsPlugin';
export type TableOfContentsEntry = [
key: NodeKey,
text: string,
tag: HeadingTagType,
];
/** @deprecated use the module @lexical/react/LexicalTableOfContentsPlugin */
export {type TableOfContentsEntry};
function toEntry(heading: HeadingNode): TableOfContentsEntry {
return [heading.getKey(), heading.getTextContent(), heading.getTag()];
}
function $insertHeadingIntoTableOfContents(
prevHeading: HeadingNode | null,
newHeading: HeadingNode | null,
currentTableOfContents: Array<TableOfContentsEntry>,
): Array<TableOfContentsEntry> {
if (newHeading === null) {
return currentTableOfContents;
}
const newEntry: TableOfContentsEntry = toEntry(newHeading);
let newTableOfContents: Array<TableOfContentsEntry> = [];
if (prevHeading === null) {
// check if key already exists
if (
currentTableOfContents.length > 0 &&
currentTableOfContents[0][0] === newHeading.__key
) {
return currentTableOfContents;
}
newTableOfContents = [newEntry, ...currentTableOfContents];
} else {
for (let i = 0; i < currentTableOfContents.length; i++) {
const key = currentTableOfContents[i][0];
newTableOfContents.push(currentTableOfContents[i]);
if (key === prevHeading.getKey() && key !== newHeading.getKey()) {
// check if key already exists
if (
i + 1 < currentTableOfContents.length &&
currentTableOfContents[i + 1][0] === newHeading.__key
) {
return currentTableOfContents;
}
newTableOfContents.push(newEntry);
}
}
}
return newTableOfContents;
}
function $deleteHeadingFromTableOfContents(
key: NodeKey,
currentTableOfContents: Array<TableOfContentsEntry>,
): Array<TableOfContentsEntry> {
const newTableOfContents = [];
for (const heading of currentTableOfContents) {
if (heading[0] !== key) {
newTableOfContents.push(heading);
}
}
return newTableOfContents;
}
function $updateHeadingInTableOfContents(
heading: HeadingNode,
currentTableOfContents: Array<TableOfContentsEntry>,
): Array<TableOfContentsEntry> {
const newTableOfContents: Array<TableOfContentsEntry> = [];
for (const oldHeading of currentTableOfContents) {
if (oldHeading[0] === heading.getKey()) {
newTableOfContents.push(toEntry(heading));
} else {
newTableOfContents.push(oldHeading);
}
}
return newTableOfContents;
}
/**
* Returns the updated table of contents, placing the given `heading` before the given `prevHeading`. If `prevHeading`
* is undefined, `heading` is placed at the start of table of contents
*/
function $updateHeadingPosition(
prevHeading: HeadingNode | null,
heading: HeadingNode,
currentTableOfContents: Array<TableOfContentsEntry>,
): Array<TableOfContentsEntry> {
const newTableOfContents: Array<TableOfContentsEntry> = [];
const newEntry: TableOfContentsEntry = toEntry(heading);
if (!prevHeading) {
newTableOfContents.push(newEntry);
}
for (const oldHeading of currentTableOfContents) {
if (oldHeading[0] === heading.getKey()) {
continue;
}
newTableOfContents.push(oldHeading);
if (prevHeading && oldHeading[0] === prevHeading.getKey()) {
newTableOfContents.push(newEntry);
}
}
return newTableOfContents;
}
function $getPreviousHeading(node: HeadingNode): HeadingNode | null {
let prevHeading = $getNextRightPreorderNode(node);
while (prevHeading !== null && !$isHeadingNode(prevHeading)) {
prevHeading = $getNextRightPreorderNode(prevHeading);
}
return prevHeading;
}
type Props = {
children: (
values: Array<TableOfContentsEntry>,
editor: LexicalEditor,
) => JSX.Element;
};
export default function LexicalTableOfContentsPlugin({
children,
}: Props): JSX.Element {
const [tableOfContents, setTableOfContents] = useState<
Array<TableOfContentsEntry>
>([]);
const [editor] = useLexicalComposerContext();
useEffect(() => {
// Set table of contents initial state
let currentTableOfContents: Array<TableOfContentsEntry> = [];
editor.getEditorState().read(() => {
const root = $getRoot();
const rootChildren = root.getChildren();
for (const child of rootChildren) {
if ($isHeadingNode(child)) {
currentTableOfContents.push([
child.getKey(),
child.getTextContent(),
child.getTag(),
]);
}
}
setTableOfContents(currentTableOfContents);
});
const removeRootUpdateListener = editor.registerUpdateListener(
({editorState, dirtyElements}) => {
editorState.read(() => {
const updateChildHeadings = (node: ElementNode) => {
for (const child of node.getChildren()) {
if ($isHeadingNode(child)) {
const prevHeading = $getPreviousHeading(child);
currentTableOfContents = $updateHeadingPosition(
prevHeading,
child,
currentTableOfContents,
);
setTableOfContents(currentTableOfContents);
} else if ($isElementNode(child)) {
updateChildHeadings(child);
}
}
};
// If a node is changes, all child heading positions need to be updated
$getRoot()
.getChildren()
.forEach((node) => {
if ($isElementNode(node) && dirtyElements.get(node.__key)) {
updateChildHeadings(node);
}
});
});
},
);
// Listen to updates to heading mutations and update state
const removeHeaderMutationListener = editor.registerMutationListener(
HeadingNode,
(mutatedNodes: Map<string, NodeMutation>) => {
editor.getEditorState().read(() => {
for (const [nodeKey, mutation] of mutatedNodes) {
if (mutation === 'created') {
const newHeading = $getNodeByKey<HeadingNode>(nodeKey);
if (newHeading !== null) {
const prevHeading = $getPreviousHeading(newHeading);
currentTableOfContents = $insertHeadingIntoTableOfContents(
prevHeading,
newHeading,
currentTableOfContents,
);
}
} else if (mutation === 'destroyed') {
currentTableOfContents = $deleteHeadingFromTableOfContents(
nodeKey,
currentTableOfContents,
);
} else if (mutation === 'updated') {
const newHeading = $getNodeByKey<HeadingNode>(nodeKey);
if (newHeading !== null) {
const prevHeading = $getPreviousHeading(newHeading);
currentTableOfContents = $updateHeadingPosition(
prevHeading,
newHeading,
currentTableOfContents,
);
}
}
}
setTableOfContents(currentTableOfContents);
});
},
);
// Listen to text node mutation updates
const removeTextNodeMutationListener = editor.registerMutationListener(
TextNode,
(mutatedNodes: Map<string, NodeMutation>) => {
editor.getEditorState().read(() => {
for (const [nodeKey, mutation] of mutatedNodes) {
if (mutation === 'updated') {
const currNode = $getNodeByKey(nodeKey);
if (currNode !== null) {
const parentNode = currNode.getParentOrThrow();
if ($isHeadingNode(parentNode)) {
currentTableOfContents = $updateHeadingInTableOfContents(
parentNode,
currentTableOfContents,
);
setTableOfContents(currentTableOfContents);
}
}
}
}
});
},
);
return () => {
removeHeaderMutationListener();
removeTextNodeMutationListener();
removeRootUpdateListener();
};
}, [editor]);
return children(tableOfContents, editor);
}
/** @deprecated use module @lexical/react/LexicalTableOfContentsPlugin */
// eslint-disable-next-line no-restricted-exports
export default TableOfContentsPlugin;

View File

@ -0,0 +1,267 @@
/**
* 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.
*
*/
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {$isHeadingNode, HeadingNode, HeadingTagType} from '@lexical/rich-text';
import {$getNextRightPreorderNode} from '@lexical/utils';
import {
$getNodeByKey,
$getRoot,
$isElementNode,
ElementNode,
LexicalEditor,
NodeKey,
NodeMutation,
TextNode,
} from 'lexical';
import {useEffect, useState} from 'react';
export type TableOfContentsEntry = [
key: NodeKey,
text: string,
tag: HeadingTagType,
];
function toEntry(heading: HeadingNode): TableOfContentsEntry {
return [heading.getKey(), heading.getTextContent(), heading.getTag()];
}
function $insertHeadingIntoTableOfContents(
prevHeading: HeadingNode | null,
newHeading: HeadingNode | null,
currentTableOfContents: Array<TableOfContentsEntry>,
): Array<TableOfContentsEntry> {
if (newHeading === null) {
return currentTableOfContents;
}
const newEntry: TableOfContentsEntry = toEntry(newHeading);
let newTableOfContents: Array<TableOfContentsEntry> = [];
if (prevHeading === null) {
// check if key already exists
if (
currentTableOfContents.length > 0 &&
currentTableOfContents[0][0] === newHeading.__key
) {
return currentTableOfContents;
}
newTableOfContents = [newEntry, ...currentTableOfContents];
} else {
for (let i = 0; i < currentTableOfContents.length; i++) {
const key = currentTableOfContents[i][0];
newTableOfContents.push(currentTableOfContents[i]);
if (key === prevHeading.getKey() && key !== newHeading.getKey()) {
// check if key already exists
if (
i + 1 < currentTableOfContents.length &&
currentTableOfContents[i + 1][0] === newHeading.__key
) {
return currentTableOfContents;
}
newTableOfContents.push(newEntry);
}
}
}
return newTableOfContents;
}
function $deleteHeadingFromTableOfContents(
key: NodeKey,
currentTableOfContents: Array<TableOfContentsEntry>,
): Array<TableOfContentsEntry> {
const newTableOfContents = [];
for (const heading of currentTableOfContents) {
if (heading[0] !== key) {
newTableOfContents.push(heading);
}
}
return newTableOfContents;
}
function $updateHeadingInTableOfContents(
heading: HeadingNode,
currentTableOfContents: Array<TableOfContentsEntry>,
): Array<TableOfContentsEntry> {
const newTableOfContents: Array<TableOfContentsEntry> = [];
for (const oldHeading of currentTableOfContents) {
if (oldHeading[0] === heading.getKey()) {
newTableOfContents.push(toEntry(heading));
} else {
newTableOfContents.push(oldHeading);
}
}
return newTableOfContents;
}
/**
* Returns the updated table of contents, placing the given `heading` before the given `prevHeading`. If `prevHeading`
* is undefined, `heading` is placed at the start of table of contents
*/
function $updateHeadingPosition(
prevHeading: HeadingNode | null,
heading: HeadingNode,
currentTableOfContents: Array<TableOfContentsEntry>,
): Array<TableOfContentsEntry> {
const newTableOfContents: Array<TableOfContentsEntry> = [];
const newEntry: TableOfContentsEntry = toEntry(heading);
if (!prevHeading) {
newTableOfContents.push(newEntry);
}
for (const oldHeading of currentTableOfContents) {
if (oldHeading[0] === heading.getKey()) {
continue;
}
newTableOfContents.push(oldHeading);
if (prevHeading && oldHeading[0] === prevHeading.getKey()) {
newTableOfContents.push(newEntry);
}
}
return newTableOfContents;
}
function $getPreviousHeading(node: HeadingNode): HeadingNode | null {
let prevHeading = $getNextRightPreorderNode(node);
while (prevHeading !== null && !$isHeadingNode(prevHeading)) {
prevHeading = $getNextRightPreorderNode(prevHeading);
}
return prevHeading;
}
type Props = {
children: (
values: Array<TableOfContentsEntry>,
editor: LexicalEditor,
) => JSX.Element;
};
export function TableOfContentsPlugin({children}: Props): JSX.Element {
const [tableOfContents, setTableOfContents] = useState<
Array<TableOfContentsEntry>
>([]);
const [editor] = useLexicalComposerContext();
useEffect(() => {
// Set table of contents initial state
let currentTableOfContents: Array<TableOfContentsEntry> = [];
editor.getEditorState().read(() => {
const root = $getRoot();
const rootChildren = root.getChildren();
for (const child of rootChildren) {
if ($isHeadingNode(child)) {
currentTableOfContents.push([
child.getKey(),
child.getTextContent(),
child.getTag(),
]);
}
}
setTableOfContents(currentTableOfContents);
});
const removeRootUpdateListener = editor.registerUpdateListener(
({editorState, dirtyElements}) => {
editorState.read(() => {
const updateChildHeadings = (node: ElementNode) => {
for (const child of node.getChildren()) {
if ($isHeadingNode(child)) {
const prevHeading = $getPreviousHeading(child);
currentTableOfContents = $updateHeadingPosition(
prevHeading,
child,
currentTableOfContents,
);
setTableOfContents(currentTableOfContents);
} else if ($isElementNode(child)) {
updateChildHeadings(child);
}
}
};
// If a node is changes, all child heading positions need to be updated
$getRoot()
.getChildren()
.forEach((node) => {
if ($isElementNode(node) && dirtyElements.get(node.__key)) {
updateChildHeadings(node);
}
});
});
},
);
// Listen to updates to heading mutations and update state
const removeHeaderMutationListener = editor.registerMutationListener(
HeadingNode,
(mutatedNodes: Map<string, NodeMutation>) => {
editor.getEditorState().read(() => {
for (const [nodeKey, mutation] of mutatedNodes) {
if (mutation === 'created') {
const newHeading = $getNodeByKey<HeadingNode>(nodeKey);
if (newHeading !== null) {
const prevHeading = $getPreviousHeading(newHeading);
currentTableOfContents = $insertHeadingIntoTableOfContents(
prevHeading,
newHeading,
currentTableOfContents,
);
}
} else if (mutation === 'destroyed') {
currentTableOfContents = $deleteHeadingFromTableOfContents(
nodeKey,
currentTableOfContents,
);
} else if (mutation === 'updated') {
const newHeading = $getNodeByKey<HeadingNode>(nodeKey);
if (newHeading !== null) {
const prevHeading = $getPreviousHeading(newHeading);
currentTableOfContents = $updateHeadingPosition(
prevHeading,
newHeading,
currentTableOfContents,
);
}
}
}
setTableOfContents(currentTableOfContents);
});
},
);
// Listen to text node mutation updates
const removeTextNodeMutationListener = editor.registerMutationListener(
TextNode,
(mutatedNodes: Map<string, NodeMutation>) => {
editor.getEditorState().read(() => {
for (const [nodeKey, mutation] of mutatedNodes) {
if (mutation === 'updated') {
const currNode = $getNodeByKey(nodeKey);
if (currNode !== null) {
const parentNode = currNode.getParentOrThrow();
if ($isHeadingNode(parentNode)) {
currentTableOfContents = $updateHeadingInTableOfContents(
parentNode,
currentTableOfContents,
);
setTableOfContents(currentTableOfContents);
}
}
}
}
});
},
);
return () => {
removeHeaderMutationListener();
removeTextNodeMutationListener();
removeRootUpdateListener();
};
}, [editor]);
return children(tableOfContents, editor);
}

View File

@ -12,7 +12,7 @@ import {AutoLinkNode, LinkNode} from '@lexical/link';
import {ListItemNode, ListNode} from '@lexical/list';
import {OverflowNode} from '@lexical/overflow';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
import {$rootTextContent} from '@lexical/text';

View File

@ -19,7 +19,7 @@ import {CollaborationPlugin} from '../../LexicalCollaborationPlugin';
import {LexicalComposer} from '../../LexicalComposer';
import {useLexicalComposerContext} from '../../LexicalComposerContext';
import {ContentEditable} from '../../LexicalContentEditable';
import LexicalErrorBoundary from '../../LexicalErrorBoundary';
import {LexicalErrorBoundary} from '../../LexicalErrorBoundary';
import {RichTextPlugin} from '../../LexicalRichTextPlugin';
function Editor({

View File

@ -9,7 +9,7 @@
import type {LexicalSubscription} from './useLexicalSubscription';
import type {LexicalEditor} from 'lexical';
import useLexicalSubscription from './useLexicalSubscription';
import {useLexicalSubscription} from './useLexicalSubscription';
function subscription(editor: LexicalEditor): LexicalSubscription<boolean> {
return {
@ -20,6 +20,10 @@ function subscription(editor: LexicalEditor): LexicalSubscription<boolean> {
};
}
export default function useLexicalEditable(): boolean {
export function useLexicalEditable(): boolean {
return useLexicalSubscription(subscription);
}
/** @deprecated use the named export {@link useLexicalEditable} */
// eslint-disable-next-line no-restricted-exports
export default useLexicalEditable;

View File

@ -20,7 +20,7 @@ export type LexicalSubscription<T> = {
/**
* Shortcut to Lexical subscriptions when values are used for render.
*/
export default function useLexicalSubscription<T>(
export function useLexicalSubscription<T>(
subscription: (editor: LexicalEditor) => LexicalSubscription<T>,
): T {
const [editor] = useLexicalComposerContext();
@ -46,3 +46,7 @@ export default function useLexicalSubscription<T>(
return value;
}
/** @deprecated use the named export {@link useLexicalSubscription} */
// eslint-disable-next-line no-restricted-exports
export default useLexicalSubscription;

View File

@ -10,7 +10,7 @@ import {$createLinkNode} from '@lexical/link';
import {$createListItemNode, $createListNode} from '@lexical/list';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {$createHeadingNode} from '@lexical/rich-text';

View File

@ -14,7 +14,7 @@ import {OverflowNode} from '@lexical/overflow';
import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
import {

View File

@ -43,7 +43,7 @@ $ HOST=localhost PORT=1234 YPERSISTENCE=./yjs-wss-db npx y-websocket
import {$getRoot, $createParagraphNode, $createTextNode} from 'lexical';
import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {CollaborationPlugin} from '@lexical/react/LexicalCollaborationPlugin';
import * as Y from 'yjs';

View File

@ -37,7 +37,7 @@ import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
const theme = {
// Theme styling goes here

View File

@ -49,7 +49,7 @@ import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {exampleTheme} from './exampleTheme';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
const initialConfig = {namespace: 'MyEditor', theme: exampleTheme};

View File

@ -169,20 +169,22 @@ Adds markdown shortcut support: headings, lists, code blocks, quotes, links and
<MarkdownShortcutPlugin />
```
### `TableOfContentsPlugin`
This plugin allows you to navigate to certain sections of the page by clicking on headings that exist inside these sections. Once you load the plugin, it automatically collects and injects the headings of the page inside the table of contents, then it listens to any deletions or modifications to those headings and updates the table of contents. Additionally, it's able to track any newly added headings and inserts them in the table of contents once they are created. This plugin also supports lazy loading - so you can defer adding the plugin until when the user needs it.
### `LexicalTableOfContentsPlugin`
This plugin allows you to render a table of contents for a page from the headings from the editor. It listens to any deletions or modifications to those headings and updates the table of contents. Additionally, it's able to track any newly added headings and inserts them in the table of contents once they are created. This plugin also supports lazy loading - so you can defer adding the plugin until when the user needs it.
In order to use `TableOfContentsPlugin`, you need to pass a callback function in its children. This callback function gives you access to the up-to-date data of the table of contents. You can access this data through a single parameter for the callback which comes in the form of an array of arrays `[[headingKey, headingTextContent, headingTag], [], [], ...]`
`headingKey`: Unique key that identifies the heading.
`headingTextContent`: A string of the exact text of the heading.
`headingTag`: A string that reads either 'h1', 'h2', or 'h3'.
```jsx
<TableOfContentsPlugin />
```
You can alternatively leverage the use of `LexicalTableOfContents` API, which provides you with all the functionality that `TableOfContentsPlugin` provides, but without any styling.
In order to use `LexicalTableOfContents`, you need to pass a callback function in its children. This callback function gives you access to the up-to-date data of the table of contents. You can access this data through a single parameter for the callback which comes in the form of an array of arrays `[[headingKey, headingTextContent, headingTag], [], [], ...]`
`headingKey`: Unique key that identifies the heading.`headingTextContent`: A string of the exact text of the heading.`headingTag`: A string that reads either 'h1', 'h2', or 'h3'.
```jsx
<LexicalTableOfContents>
<TableOfContentsPlugin>
{(tableOfContentsArray) => {
return <MyCustomTableOfContetsPlugin tableOfContents={tableOfContentsArray} />;
return <MyCustomTableOfContentsPlugin tableOfContents={tableOfContentsArray} />;
}}
</LexicalTableOfContents>
</TableOfContentsPlugin>
```
### `LexicalEditorRefPlugin`

View File

@ -447,7 +447,7 @@ export function createEditor(editorConfig?: CreateEditorArgs): LexicalEditor {
// Ensure custom nodes implement required methods.
if (__DEV__) {
const name = klass.name;
if (name !== 'RootNode') {
if (name !== 'RootNode' && name !== 'ArtificialNode__DO_NOT_USE') {
const proto = klass.prototype;
['getType', 'clone'].forEach((method) => {
// eslint-disable-next-line no-prototype-builtins

View File

@ -8,7 +8,7 @@
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {
$createTableCellNode,

View File

@ -8,7 +8,7 @@
import {ListItemNode, ListNode} from '@lexical/list';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {ListPlugin} from '@lexical/react/LexicalListPlugin';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {

View File

@ -8,7 +8,7 @@
import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import {

View File

@ -9,7 +9,7 @@
import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import {

View File

@ -191,7 +191,7 @@ async function build(name, inputFile, outputPath, outputFile, isProd, format) {
tsconfig: path.resolve('./tsconfig.build.json'),
},
],
'@babel/preset-react',
['@babel/preset-react', {runtime: 'automatic'}],
],
}),
{
@ -238,7 +238,7 @@ async function build(name, inputFile, outputPath, outputFile, isProd, format) {
};
const outputOptions = {
esModule: false,
exports: 'auto',
exports: 'named',
externalLiveBindings: false,
file: outputFile,
format, // change between es and cjs modules

View File

@ -92,8 +92,6 @@ async function expectTransform(opts) {
const {code} = babel.transform(fmt`${opts.codeBefore}`, {
plugins: [[transformErrorMessages, {errorCodesPath, ...opts.opts}]],
});
const afterCode = fmt`${code}`;
console.log({afterCode, code});
expect(fmt`${code}`).toEqual(fmt`${opts.codeExpect}`);
},
);

View File

@ -23,7 +23,7 @@ const BLOCK_REGEX =
function flowTemplate(wwwName) {
return (
headerTemplate.replace(/^( *\/)$/, '* @flow strict\n$1') +
headerTemplate.replace(/^( \*\/)$/m, '* @flow strict\n$1') +
`
/**
* ${wwwName}

View File

@ -124,6 +124,9 @@
"@lexical/react/LexicalTableOfContents": [
"./packages/lexical-react/src/LexicalTableOfContents.tsx"
],
"@lexical/react/LexicalTableOfContentsPlugin": [
"./packages/lexical-react/src/LexicalTableOfContentsPlugin.tsx"
],
"@lexical/react/LexicalTablePlugin": [
"./packages/lexical-react/src/LexicalTablePlugin.ts"
],

View File

@ -132,6 +132,9 @@
"@lexical/react/LexicalTableOfContents": [
"./packages/lexical-react/src/LexicalTableOfContents.tsx"
],
"@lexical/react/LexicalTableOfContentsPlugin": [
"./packages/lexical-react/src/LexicalTableOfContentsPlugin.tsx"
],
"@lexical/react/LexicalTablePlugin": [
"./packages/lexical-react/src/LexicalTablePlugin.ts"
],