Skip to content

Commit

Permalink
Merge branch 'main' into fix-strikethrough-issue
Browse files Browse the repository at this point in the history
  • Loading branch information
vantage-ola authored Dec 7, 2024
2 parents 621e1b3 + 7776cea commit 1ed4218
Show file tree
Hide file tree
Showing 21 changed files with 1,266 additions and 312 deletions.
12 changes: 9 additions & 3 deletions packages/lexical-playground/__tests__/e2e/Tables.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3737,7 +3737,9 @@ test.describe.parallel('Tables', () => {
<span data-lexical-text="true">Hello world</span>
</p>
</td>
<td class="PlaygroundEditorTheme__tableCell"><br /></td>
<td class="PlaygroundEditorTheme__tableCell">
<p class="PlaygroundEditorTheme__paragraph"><br /></p>
</td>
<td class="PlaygroundEditorTheme__tableCell">
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
Expand All @@ -3747,8 +3749,12 @@ test.describe.parallel('Tables', () => {
</td>
</tr>
<tr>
<td class="PlaygroundEditorTheme__tableCell"><br /></td>
<td class="PlaygroundEditorTheme__tableCell"><br /></td>
<td class="PlaygroundEditorTheme__tableCell">
<p class="PlaygroundEditorTheme__paragraph"><br /></p>
</td>
<td class="PlaygroundEditorTheme__tableCell">
<p class="PlaygroundEditorTheme__paragraph"><br /></p>
</td>
<td class="PlaygroundEditorTheme__tableCell">
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,25 @@ export function LayoutPlugin(): null {
return false;
};

const $fillLayoutItemIfEmpty = (node: LayoutItemNode) => {
if (node.isEmpty()) {
node.append($createParagraphNode());
}
};

const $removeIsolatedLayoutItem = (node: LayoutItemNode): boolean => {
const parent = node.getParent<ElementNode>();
if (!$isLayoutContainerNode(parent)) {
const children = node.getChildren<LexicalNode>();
for (const child of children) {
node.insertBefore(child);
}
node.remove();
return true;
}
return false;
};

return mergeRegister(
// When layout is the last child pressing down/right arrow will insert paragraph
// below it to allow adding more content. It's similar what $insertBlockNode
Expand Down Expand Up @@ -186,17 +205,17 @@ export function LayoutPlugin(): null {
},
COMMAND_PRIORITY_EDITOR,
),
// Structure enforcing transformers for each node type. In case nesting structure is not
// "Container > Item" it'll unwrap nodes and convert it back
// to regular content.

editor.registerNodeTransform(LayoutItemNode, (node) => {
const parent = node.getParent<ElementNode>();
if (!$isLayoutContainerNode(parent)) {
const children = node.getChildren<LexicalNode>();
for (const child of children) {
node.insertBefore(child);
}
node.remove();
// Structure enforcing transformers for each node type. In case nesting structure is not
// "Container > Item" it'll unwrap nodes and convert it back
// to regular content.
const isRemoved = $removeIsolatedLayoutItem(node);

if (!isRemoved) {
// Layout item should always have a child. this function will listen
// for any empty layout item and fill it with a paragraph node
$fillLayoutItemIfEmpty(node);
}
}),
editor.registerNodeTransform(LayoutContainerNode, (node) => {
Expand Down
46 changes: 11 additions & 35 deletions packages/lexical-playground/src/plugins/TablePlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,13 @@

import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {
$createTableNodeWithDimensions,
INSERT_TABLE_COMMAND,
TableCellNode,
TableNode,
TableRowNode,
} from '@lexical/table';
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
EditorThemeClasses,
Klass,
LexicalCommand,
LexicalEditor,
LexicalNode,
} from 'lexical';
import {EditorThemeClasses, Klass, LexicalEditor, LexicalNode} from 'lexical';
import {createContext, useContext, useEffect, useMemo, useState} from 'react';
import * as React from 'react';
import invariant from 'shared/invariant';

import Button from '../ui/Button';
Expand Down Expand Up @@ -53,9 +44,6 @@ export type CellEditorConfig = Readonly<{
theme?: EditorThemeClasses;
}>;

export const INSERT_NEW_TABLE_COMMAND: LexicalCommand<InsertTableCommandPayload> =
createCommand('INSERT_NEW_TABLE_COMMAND');

export const CellContext = createContext<CellContextShape>({
cellEditorConfig: null,
cellEditorPlugins: null,
Expand Down Expand Up @@ -155,28 +143,16 @@ export function TablePlugin({
}): JSX.Element | null {
const [editor] = useLexicalComposerContext();
const cellContext = useContext(CellContext);

useEffect(() => {
if (!editor.hasNodes([TableNode])) {
invariant(false, 'TablePlugin: TableNode is not registered on editor');
if (!editor.hasNodes([TableNode, TableRowNode, TableCellNode])) {
invariant(
false,
'TablePlugin: TableNode, TableRowNode, or TableCellNode is not registered on editor',
);
}

}, [editor]);
useEffect(() => {
cellContext.set(cellEditorConfig, children);

return editor.registerCommand<InsertTableCommandPayload>(
INSERT_NEW_TABLE_COMMAND,
({columns, rows, includeHeaders}) => {
const tableNode = $createTableNodeWithDimensions(
Number(rows),
Number(columns),
includeHeaders,
);
$insertNodes([tableNode]);
return true;
},
COMMAND_PRIORITY_EDITOR,
);
}, [cellContext, cellEditorConfig, children, editor]);

}, [cellContext, cellEditorConfig, children]);
return null;
}
211 changes: 10 additions & 201 deletions packages/lexical-react/src/LexicalTablePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,43 +6,15 @@
*
*/

import type {
HTMLTableElementWithWithTableSelectionState,
InsertTableCommandPayload,
TableObserver,
} from '@lexical/table';
import type {NodeKey} from 'lexical';

import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {
$computeTableMap,
$computeTableMapSkipCellCheck,
$createTableCellNode,
$createTableNodeWithDimensions,
$getNodeTriplet,
$getTableAndElementByKey,
$isTableCellNode,
$isTableRowNode,
applyTableHandlers,
getTableElement,
INSERT_TABLE_COMMAND,
registerTableCellUnmergeTransform,
registerTablePlugin,
registerTableSelectionObserver,
setScrollableTablesActive,
TableCellNode,
TableNode,
TableRowNode,
} from '@lexical/table';
import {
$insertFirst,
$insertNodeToNearestRoot,
mergeRegister,
} from '@lexical/utils';
import {
$createParagraphNode,
$isTextNode,
COMMAND_PRIORITY_EDITOR,
} from 'lexical';
import {useEffect} from 'react';
import invariant from 'shared/invariant';

export interface TablePluginProps {
/**
Expand Down Expand Up @@ -82,181 +54,18 @@ export function TablePlugin({
setScrollableTablesActive(editor, hasHorizontalScroll);
}, [editor, hasHorizontalScroll]);

useEffect(() => {
if (!editor.hasNodes([TableNode, TableCellNode, TableRowNode])) {
invariant(
false,
'TablePlugin: TableNode, TableCellNode or TableRowNode not registered on editor',
);
}

return mergeRegister(
editor.registerCommand<InsertTableCommandPayload>(
INSERT_TABLE_COMMAND,
({columns, rows, includeHeaders}) => {
const tableNode = $createTableNodeWithDimensions(
Number(rows),
Number(columns),
includeHeaders,
);
$insertNodeToNearestRoot(tableNode);

const firstDescendant = tableNode.getFirstDescendant();
if ($isTextNode(firstDescendant)) {
firstDescendant.select();
}

return true;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerNodeTransform(TableNode, (node) => {
const [gridMap] = $computeTableMapSkipCellCheck(node, null, null);
const maxRowLength = gridMap.reduce((curLength, row) => {
return Math.max(curLength, row.length);
}, 0);
const rowNodes = node.getChildren();
for (let i = 0; i < gridMap.length; ++i) {
const rowNode = rowNodes[i];
if (!rowNode) {
continue;
}
const rowLength = gridMap[i].reduce(
(acc, cell) => (cell ? 1 + acc : acc),
0,
);
if (rowLength === maxRowLength) {
continue;
}
for (let j = rowLength; j < maxRowLength; ++j) {
// TODO: inherit header state from another header or body
const newCell = $createTableCellNode(0);
newCell.append($createParagraphNode());
(rowNode as TableRowNode).append(newCell);
}
}
}),
);
}, [editor]);
useEffect(() => registerTablePlugin(editor), [editor]);

useEffect(() => {
const tableSelections = new Map<
NodeKey,
[TableObserver, HTMLTableElementWithWithTableSelectionState]
>();

const initializeTableNode = (
tableNode: TableNode,
nodeKey: NodeKey,
dom: HTMLElement,
) => {
const tableElement = getTableElement(tableNode, dom);
const tableSelection = applyTableHandlers(
tableNode,
tableElement,
editor,
hasTabHandler,
);
tableSelections.set(nodeKey, [tableSelection, tableElement]);
};

const unregisterMutationListener = editor.registerMutationListener(
TableNode,
(nodeMutations) => {
editor.getEditorState().read(
() => {
for (const [nodeKey, mutation] of nodeMutations) {
const tableSelection = tableSelections.get(nodeKey);
if (mutation === 'created' || mutation === 'updated') {
const {tableNode, tableElement} =
$getTableAndElementByKey(nodeKey);
if (tableSelection === undefined) {
initializeTableNode(tableNode, nodeKey, tableElement);
} else if (tableElement !== tableSelection[1]) {
// The update created a new DOM node, destroy the existing TableObserver
tableSelection[0].removeListeners();
tableSelections.delete(nodeKey);
initializeTableNode(tableNode, nodeKey, tableElement);
}
} else if (mutation === 'destroyed') {
if (tableSelection !== undefined) {
tableSelection[0].removeListeners();
tableSelections.delete(nodeKey);
}
}
}
},
{editor},
);
},
{skipInitialization: false},
);

return () => {
unregisterMutationListener();
// Hook might be called multiple times so cleaning up tables listeners as well,
// as it'll be reinitialized during recurring call
for (const [, [tableSelection]] of tableSelections) {
tableSelection.removeListeners();
}
};
}, [editor, hasTabHandler]);
useEffect(
() => registerTableSelectionObserver(editor, hasTabHandler),
[editor, hasTabHandler],
);

// Unmerge cells when the feature isn't enabled
useEffect(() => {
if (hasCellMerge) {
return;
if (!hasCellMerge) {
return registerTableCellUnmergeTransform(editor);
}
return editor.registerNodeTransform(TableCellNode, (node) => {
if (node.getColSpan() > 1 || node.getRowSpan() > 1) {
// When we have rowSpan we have to map the entire Table to understand where the new Cells
// fit best; let's analyze all Cells at once to save us from further transform iterations
const [, , gridNode] = $getNodeTriplet(node);
const [gridMap] = $computeTableMap(gridNode, node, node);
// TODO this function expects Tables to be normalized. Look into this once it exists
const rowsCount = gridMap.length;
const columnsCount = gridMap[0].length;
let row = gridNode.getFirstChild();
invariant(
$isTableRowNode(row),
'Expected TableNode first child to be a RowNode',
);
const unmerged = [];
for (let i = 0; i < rowsCount; i++) {
if (i !== 0) {
row = row.getNextSibling();
invariant(
$isTableRowNode(row),
'Expected TableNode first child to be a RowNode',
);
}
let lastRowCell: null | TableCellNode = null;
for (let j = 0; j < columnsCount; j++) {
const cellMap = gridMap[i][j];
const cell = cellMap.cell;
if (cellMap.startRow === i && cellMap.startColumn === j) {
lastRowCell = cell;
unmerged.push(cell);
} else if (cell.getColSpan() > 1 || cell.getRowSpan() > 1) {
invariant(
$isTableCellNode(cell),
'Expected TableNode cell to be a TableCellNode',
);
const newCell = $createTableCellNode(cell.__headerState);
if (lastRowCell !== null) {
lastRowCell.insertAfter(newCell);
} else {
$insertFirst(row, newCell);
}
}
}
}
for (const cell of unmerged) {
cell.setColSpan(1);
cell.setRowSpan(1);
}
}
});
}, [editor, hasCellMerge]);

// Remove cell background color when feature is disabled
Expand Down
Loading

0 comments on commit 1ed4218

Please sign in to comment.