Skip to content

Commit

Permalink
[lexical-playground] Table Hover Action Buttons (#6355)
Browse files Browse the repository at this point in the history
Co-authored-by: Ivaylo Pavlov <[email protected]>
  • Loading branch information
ivailop7 and Ivaylo Pavlov authored Jul 3, 2024
1 parent 55cc980 commit 4e8c7cc
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 13 deletions.
2 changes: 2 additions & 0 deletions packages/lexical-playground/src/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import SpeechToTextPlugin from './plugins/SpeechToTextPlugin';
import TabFocusPlugin from './plugins/TabFocusPlugin';
import TableCellActionMenuPlugin from './plugins/TableActionMenuPlugin';
import TableCellResizer from './plugins/TableCellResizer';
import TableHoverActionsPlugin from './plugins/TableHoverActionsPlugin';
import TableOfContentsPlugin from './plugins/TableOfContentsPlugin';
import ToolbarPlugin from './plugins/ToolbarPlugin';
import TreeViewPlugin from './plugins/TreeViewPlugin';
Expand Down Expand Up @@ -185,6 +186,7 @@ export default function Editor(): JSX.Element {
hasCellBackgroundColor={tableCellBackgroundColor}
/>
<TableCellResizer />
<TableHoverActionsPlugin />
<ImagesPlugin />
<InlineImagePlugin />
<LinkPlugin />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
/**
* 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 {
$getTableColumnIndexFromTableCellNode,
$getTableRowIndexFromTableCellNode,
$insertTableColumn__EXPERIMENTAL,
$insertTableRow__EXPERIMENTAL,
$isTableCellNode,
$isTableNode,
TableCellNode,
TableNode,
TableRowNode,
} from '@lexical/table';
import {$findMatchingParent, mergeRegister} from '@lexical/utils';
import {$getNearestNodeFromDOMNode} from 'lexical';
import {useEffect, useRef, useState} from 'react';
import * as React from 'react';
import {createPortal} from 'react-dom';

import {useDebounce} from '../CodeActionMenuPlugin/utils';

const BUTTON_WIDTH_PX = 20;

function TableHoverActionsContainer({
anchorElem,
}: {
anchorElem: HTMLElement;
}): JSX.Element {
const [editor] = useLexicalComposerContext();
const [isShownRow, setShownRow] = useState<boolean>(false);
const [isShownColumn, setShownColumn] = useState<boolean>(false);
const [shouldListenMouseMove, setShouldListenMouseMove] =
useState<boolean>(false);
const [position, setPosition] = useState({});
const codeSetRef = useRef<Set<string>>(new Set());
const tableDOMNodeRef = useRef<HTMLElement | null>(null);

const debouncedOnMouseMove = useDebounce(
(event: MouseEvent) => {
const {isOutside, tableDOMNode} = getMouseInfo(event);

if (isOutside) {
setShownRow(false);
setShownColumn(false);
return;
}

if (!tableDOMNode) {
return;
}

tableDOMNodeRef.current = tableDOMNode;

let hoveredRowNode: TableCellNode | null = null;
let hoveredColumnNode: TableCellNode | null = null;
let tableDOMElement: HTMLElement | null = null;

editor.update(() => {
const maybeTableCell = $getNearestNodeFromDOMNode(tableDOMNode);

if ($isTableCellNode(maybeTableCell)) {
const table = $findMatchingParent(maybeTableCell, (node) =>
$isTableNode(node),
);
if (!$isTableNode(table)) {
return;
}

tableDOMElement = editor.getElementByKey(table?.getKey());

if (tableDOMElement) {
const rowCount = table.getChildrenSize();
const colCount = (
(table as TableNode).getChildAtIndex(0) as TableRowNode
)?.getChildrenSize();

const rowIndex = $getTableRowIndexFromTableCellNode(maybeTableCell);
const colIndex =
$getTableColumnIndexFromTableCellNode(maybeTableCell);

if (rowIndex === rowCount - 1) {
hoveredRowNode = maybeTableCell;
} else if (colIndex === colCount - 1) {
hoveredColumnNode = maybeTableCell;
}
}
}
});

if (tableDOMElement) {
const {
width: tableElemWidth,
y: tableElemY,
x: tableElemX,
right: tableElemRight,
bottom: tableElemBottom,
height: tableElemHeight,
} = (tableDOMElement as HTMLTableElement).getBoundingClientRect();

const {y: editorElemY} = anchorElem.getBoundingClientRect();

if (hoveredRowNode) {
setShownRow(true);
setPosition({
height: BUTTON_WIDTH_PX,
left: tableElemX,
top: tableElemBottom - editorElemY + 5,
width: tableElemWidth,
});
} else if (hoveredColumnNode) {
setShownColumn(true);
setPosition({
height: tableElemHeight,
left: tableElemRight + 5,
top: tableElemY - editorElemY,
width: BUTTON_WIDTH_PX,
});
}
}
},
50,
250,
);

useEffect(() => {
if (!shouldListenMouseMove) {
return;
}

document.addEventListener('mousemove', debouncedOnMouseMove);

return () => {
setShownRow(false);
setShownColumn(false);
debouncedOnMouseMove.cancel();
document.removeEventListener('mousemove', debouncedOnMouseMove);
};
}, [shouldListenMouseMove, debouncedOnMouseMove]);

useEffect(() => {
return mergeRegister(
editor.registerMutationListener(TableNode, (mutations) => {
editor.getEditorState().read(() => {
for (const [key, type] of mutations) {
switch (type) {
case 'created':
codeSetRef.current.add(key);
setShouldListenMouseMove(codeSetRef.current.size > 0);
break;

case 'destroyed':
codeSetRef.current.delete(key);
setShouldListenMouseMove(codeSetRef.current.size > 0);
break;

default:
break;
}
}
});
}),
);
}, []);

Check warning on line 170 in packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx

View workflow job for this annotation

GitHub Actions / core-tests / integrity (20.11.0)

React Hook useEffect has a missing dependency: 'editor'. Either include it or remove the dependency array

const insertAction = (insertRow: boolean) => {
editor.update(() => {
if (tableDOMNodeRef.current) {
const maybeTableNode = $getNearestNodeFromDOMNode(
tableDOMNodeRef.current,
);
maybeTableNode?.selectEnd();
if (insertRow) {
$insertTableRow__EXPERIMENTAL();
setShownRow(false);
} else {
$insertTableColumn__EXPERIMENTAL();
setShownColumn(false);
}
}
});
};

return (
<>
{isShownRow && (
<button
className={'PlaygroundEditorTheme__tableAddRows'}
style={{...position}}
onClick={() => insertAction(true)}
/>
)}
{isShownColumn && (
<button
className={'PlaygroundEditorTheme__tableAddColumns'}
style={{...position}}
onClick={() => insertAction(false)}
/>
)}
</>
);
}

function getMouseInfo(event: MouseEvent): {
tableDOMNode: HTMLElement | null;
isOutside: boolean;
} {
const target = event.target;

if (target && target instanceof HTMLElement) {
const tableDOMNode = target.closest<HTMLElement>(
'td.PlaygroundEditorTheme__tableCell, th.PlaygroundEditorTheme__tableCell',
);

const isOutside = !(
tableDOMNode ||
target.closest<HTMLElement>(
'button.PlaygroundEditorTheme__tableAddRows',
) ||
target.closest<HTMLElement>(
'button.PlaygroundEditorTheme__tableAddColumns',
) ||
target.closest<HTMLElement>('div.TableCellResizer__resizer')
);

return {isOutside, tableDOMNode};
} else {
return {isOutside: true, tableDOMNode: null};
}
}

export default function TableHoverActionsPlugin({
anchorElem = document.body,
}: {
anchorElem?: HTMLElement;
}): React.ReactPortal | null {
return createPortal(
<TableHoverActionsContainer anchorElem={anchorElem} />,
anchorElem,
);
}
14 changes: 3 additions & 11 deletions packages/lexical-playground/src/themes/PlaygroundEditorTheme.css
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
overflow-x: scroll;
table-layout: fixed;
width: max-content;
margin: 30px 0;
margin: 0px 25px 30px 0px;
}
.PlaygroundEditorTheme__tableSelection *::selection {
background-color: transparent;
Expand Down Expand Up @@ -183,11 +183,8 @@
}
.PlaygroundEditorTheme__tableAddColumns {
position: absolute;
top: 0;
width: 20px;
background-color: #eee;
height: 100%;
right: -25px;
animation: table-controls 0.2s ease;
border: 0;
cursor: pointer;
Expand All @@ -206,16 +203,14 @@
height: 100%;
opacity: 0.4;
}
.PlaygroundEditorTheme__tableAddColumns:hover {
.PlaygroundEditorTheme__tableAddColumns:hover,
.PlaygroundEditorTheme__tableAddRows:hover {
background-color: #c9dbf0;
}
.PlaygroundEditorTheme__tableAddRows {
position: absolute;
bottom: -25px;
width: calc(100% - 25px);
background-color: #eee;
height: 20px;
left: 0;
animation: table-controls 0.2s ease;
border: 0;
cursor: pointer;
Expand All @@ -234,9 +229,6 @@
height: 100%;
opacity: 0.4;
}
.PlaygroundEditorTheme__tableAddRows:hover {
background-color: #c9dbf0;
}
@keyframes table-controls {
0% {
opacity: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,6 @@ const theme: EditorThemeClasses = {
quote: 'PlaygroundEditorTheme__quote',
rtl: 'PlaygroundEditorTheme__rtl',
table: 'PlaygroundEditorTheme__table',
tableAddColumns: 'PlaygroundEditorTheme__tableAddColumns',
tableAddRows: 'PlaygroundEditorTheme__tableAddRows',
tableCell: 'PlaygroundEditorTheme__tableCell',
tableCellActionButton: 'PlaygroundEditorTheme__tableCellActionButton',
tableCellActionButtonContainer:
Expand Down

0 comments on commit 4e8c7cc

Please sign in to comment.