From 5bbbe849bd229e1db0e7b536e6a919520ada7bb2 Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Fri, 2 Jan 2026 23:05:52 +0200 Subject: [PATCH] [lexical-playground]: Column Sort for Basic Table (#8060) Co-authored-by: Bob Ippolito --- packages/lexical-playground/src/Editor.tsx | 2 +- .../src/images/icons/filter-left.svg | 3 + .../FloatingTableTopBorderPlugin/index.css | 29 ---- .../TableHoverActionsV2Plugin/index.css | 77 +++++++++ .../index.tsx | 154 ++++++++++++++++-- .../lexical-playground/src/ui/DropDown.tsx | 4 +- 6 files changed, 226 insertions(+), 43 deletions(-) create mode 100644 packages/lexical-playground/src/images/icons/filter-left.svg delete mode 100644 packages/lexical-playground/src/plugins/FloatingTableTopBorderPlugin/index.css create mode 100644 packages/lexical-playground/src/plugins/TableHoverActionsV2Plugin/index.css rename packages/lexical-playground/src/plugins/{FloatingTableTopBorderPlugin => TableHoverActionsV2Plugin}/index.tsx (69%) diff --git a/packages/lexical-playground/src/Editor.tsx b/packages/lexical-playground/src/Editor.tsx index c8eef486825..1284efb2da1 100644 --- a/packages/lexical-playground/src/Editor.tsx +++ b/packages/lexical-playground/src/Editor.tsx @@ -59,7 +59,6 @@ import EquationsPlugin from './plugins/EquationsPlugin'; import ExcalidrawPlugin from './plugins/ExcalidrawPlugin'; import FigmaPlugin from './plugins/FigmaPlugin'; import FloatingLinkEditorPlugin from './plugins/FloatingLinkEditorPlugin'; -import TableHoverActionsV2Plugin from './plugins/FloatingTableTopBorderPlugin'; import FloatingTextFormatToolbarPlugin from './plugins/FloatingTextFormatToolbarPlugin'; import ImagesPlugin from './plugins/ImagesPlugin'; import KeywordsPlugin from './plugins/KeywordsPlugin'; @@ -76,6 +75,7 @@ import SpeechToTextPlugin from './plugins/SpeechToTextPlugin'; import TabFocusPlugin from './plugins/TabFocusPlugin'; import TableCellActionMenuPlugin from './plugins/TableActionMenuPlugin'; import TableCellResizer from './plugins/TableCellResizer'; +import TableHoverActionsV2Plugin from './plugins/TableHoverActionsV2Plugin'; import TableOfContentsPlugin from './plugins/TableOfContentsPlugin'; import TableScrollShadowPlugin from './plugins/TableScrollShadowPlugin'; import ToolbarPlugin from './plugins/ToolbarPlugin'; diff --git a/packages/lexical-playground/src/images/icons/filter-left.svg b/packages/lexical-playground/src/images/icons/filter-left.svg new file mode 100644 index 00000000000..9049b0f9ea6 --- /dev/null +++ b/packages/lexical-playground/src/images/icons/filter-left.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/lexical-playground/src/plugins/FloatingTableTopBorderPlugin/index.css b/packages/lexical-playground/src/plugins/FloatingTableTopBorderPlugin/index.css deleted file mode 100644 index 6083a17df05..00000000000 --- a/packages/lexical-playground/src/plugins/FloatingTableTopBorderPlugin/index.css +++ /dev/null @@ -1,29 +0,0 @@ -/** - * 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. - * - */ - -.floating-add-indicator { - background-image: url(../../images/icons/plus.svg); - background-color: #ffffff; - background-position: center; - background-repeat: no-repeat; - background-size: 12px 12px; - border: 1px solid #d0d0d0; - border-radius: 2px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); - box-sizing: border-box; - height: 18px; - pointer-events: auto; - transition: opacity 80ms ease; - width: 18px; - z-index: 10; - cursor: pointer; -} - -.floating-add-indicator:hover { - background-color: #f3f3f3; -} diff --git a/packages/lexical-playground/src/plugins/TableHoverActionsV2Plugin/index.css b/packages/lexical-playground/src/plugins/TableHoverActionsV2Plugin/index.css new file mode 100644 index 00000000000..52f488eb5c4 --- /dev/null +++ b/packages/lexical-playground/src/plugins/TableHoverActionsV2Plugin/index.css @@ -0,0 +1,77 @@ +/** + * 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. + * + */ + +.floating-add-indicator { + background-image: url(../../images/icons/plus.svg); + background-color: #ffffff; + background-position: center; + background-repeat: no-repeat; + background-size: 12px 12px; + border: 1px solid #d0d0d0; + border-radius: 2px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); + box-sizing: border-box; + height: 18px; + pointer-events: auto; + transition: opacity 80ms ease; + width: 18px; + z-index: 10; + cursor: pointer; +} + +.floating-filter-indicator { + background-image: url(../../images/icons/filter-left.svg); + background-color: #ffffff; + background-position: center; + background-repeat: no-repeat; + background-size: 12px 12px; + border: 1px solid #d0d0d0; + border-radius: 2px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); + box-sizing: border-box; + height: 18px; + pointer-events: auto; + transition: opacity 80ms ease; + width: 18px; + z-index: 10; + cursor: pointer; +} + +.floating-filter-indicator:hover { + background-color: #f3f3f3; +} + +.floating-add-indicator:hover { + background-color: #f3f3f3; +} + +.floating-top-actions { + display: flex; + align-items: center; + gap: 4px; + position: relative; +} + +.floating-filter-container { + position: relative; +} + +.floating-sort-menu__item { + width: 100%; + padding: 6px 12px; + background: transparent; + border: none; + text-align: left; + font-size: 12px; + color: #1f1f1f; + cursor: pointer; +} + +.floating-sort-menu__item:hover { + background-color: #f3f3f3; +} diff --git a/packages/lexical-playground/src/plugins/FloatingTableTopBorderPlugin/index.tsx b/packages/lexical-playground/src/plugins/TableHoverActionsV2Plugin/index.tsx similarity index 69% rename from packages/lexical-playground/src/plugins/FloatingTableTopBorderPlugin/index.tsx rename to packages/lexical-playground/src/plugins/TableHoverActionsV2Plugin/index.tsx index 1022ffbbac2..b6122f10699 100644 --- a/packages/lexical-playground/src/plugins/FloatingTableTopBorderPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/TableHoverActionsV2Plugin/index.tsx @@ -5,28 +5,39 @@ * LICENSE file in the root directory of this source tree. * */ - -import type {VirtualElement} from '@floating-ui/react'; import type {JSX} from 'react'; import './index.css'; -import {autoUpdate, offset, shift, useFloating} from '@floating-ui/react'; +import { + autoUpdate, + offset, + shift, + useFloating, + type VirtualElement, +} from '@floating-ui/react'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import {useLexicalEditable} from '@lexical/react/useLexicalEditable'; import { + $computeTableMapSkipCellCheck, $insertTableColumnAtSelection, $insertTableRowAtSelection, $isTableCellNode, + $isTableNode, + $isTableRowNode, + type TableNode, } from '@lexical/table'; import { + $getChildCaret, $getNearestNodeFromDOMNode, - EditorThemeClasses, + $getSiblingCaret, + type EditorThemeClasses, isHTMLElement, } from 'lexical'; import {useEffect, useRef, useState} from 'react'; import {createPortal} from 'react-dom'; +import DropDown, {DropDownItem} from '../../ui/DropDown'; import {getThemeSelector} from '../../utils/getThemeSelector'; const INDICATOR_SIZE_PX = 18; @@ -34,6 +45,39 @@ const SIDE_INDICATOR_SIZE_PX = 18; const TOP_BUTTON_OVERHANG = INDICATOR_SIZE_PX / 2; const LEFT_BUTTON_OVERHANG = SIDE_INDICATOR_SIZE_PX / 2; +/** + * Checks if the table does not have any merged cells. + * + * @param table Table to check for if it has any merged cells. + * @returns True if the table does not have any merged cells, false otherwise. + */ +function $isSimpleTable(table: TableNode): boolean { + const rows = table.getChildren(); + let columns: null | number = null; + for (const row of rows) { + if (!$isTableRowNode(row)) { + return false; + } + if (columns === null) { + columns = row.getChildrenSize(); + } + if (row.getChildrenSize() !== columns) { + return false; + } + const cells = row.getChildren(); + for (const cell of cells) { + if ( + !$isTableCellNode(cell) || + cell.getRowSpan() !== 1 || + cell.getColSpan() !== 1 + ) { + return false; + } + } + } + return (columns || 0) > 0; +} + function getTableFromMouseEvent( event: MouseEvent, getTheme: () => EditorThemeClasses | null | undefined, @@ -148,8 +192,12 @@ function TableHoverActionsV2({ const handleMouseMove = (event: MouseEvent) => { if ( - event.target === floatingElemRef.current || - event.target === leftFloatingElemRef.current + (floatingElemRef.current && + event.target instanceof Node && + floatingElemRef.current.contains(event.target)) || + (leftFloatingElemRef.current && + event.target instanceof Node && + leftFloatingElemRef.current.contains(event.target)) ) { return; } @@ -294,9 +342,73 @@ function TableHoverActionsV2({ }); }; + const handleSortColumn = (direction: 'asc' | 'desc') => { + const targetCell = hoveredTopCellRef.current; + if (!targetCell) { + return; + } + + editor.update(() => { + const cellNode = $getNearestNodeFromDOMNode(targetCell); + if (!$isTableCellNode(cellNode)) { + return; + } + + const rowNode = cellNode.getParent(); + if (!rowNode || !$isTableRowNode(rowNode)) { + return; + } + + const tableNode = rowNode.getParent(); + if (!$isTableNode(tableNode) || !$isSimpleTable(tableNode)) { + return; + } + + const colIndex = cellNode.getIndexWithinParent(); + const rows = tableNode.getChildren().filter($isTableRowNode); + + const [tableMap] = $computeTableMapSkipCellCheck( + tableNode, + cellNode, + cellNode, + ); + + const headerCell = tableMap[0]?.[colIndex]?.cell; + const shouldSkipTopRow = headerCell?.hasHeader() ?? false; + + const sortableRows = shouldSkipTopRow ? rows.slice(1) : rows; + + if (sortableRows.length <= 1) { + return; + } + + sortableRows.sort((a, b) => { + const aRowIndex = rows.indexOf(a); + const bRowIndex = rows.indexOf(b); + + const aMapRow = tableMap[aRowIndex] ?? []; + const bMapRow = tableMap[bRowIndex] ?? []; + + const aCellValue = aMapRow[colIndex]; + const bCellValue = bMapRow[colIndex]; + + const aText = aCellValue?.cell.getTextContent() ?? ''; + const bText = bCellValue?.cell.getTextContent() ?? ''; + const result = aText.localeCompare(bText, undefined, {numeric: true}); + return direction === 'asc' ? -result : result; + }); + + const insertionCaret = shouldSkipTopRow + ? $getSiblingCaret(rows[0], 'next') + : $getChildCaret(tableNode, 'next'); + + insertionCaret?.splice(0, sortableRows); + }); + }; + return ( <> - {showDropDown &&