Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/lexical-playground/src/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down
3 changes: 3 additions & 0 deletions packages/lexical-playground/src/images/icons/filter-left.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,79 @@
* 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;
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,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 (
<>
<button
<div
ref={(node) => {
floatingElemRef.current = node;
refs.setFloating(node);
Expand All @@ -305,11 +417,29 @@ function TableHoverActionsV2({
...floatingStyles,
opacity: isVisible ? 1 : 0,
}}
className="floating-add-indicator"
aria-label="Add column"
type="button"
onClick={handleAddColumn}
/>
className="floating-top-actions">
<DropDown
buttonAriaLabel="Sort column"
buttonClassName="floating-filter-indicator"
hideChevron={true}>
<DropDownItem
className="item"
onClick={() => handleSortColumn('desc')}>
Sort Ascending
</DropDownItem>
<DropDownItem
className="item"
onClick={() => handleSortColumn('asc')}>
Sort Descending
</DropDownItem>
</DropDown>
<button
className="floating-add-indicator"
aria-label="Add column"
type="button"
onClick={handleAddColumn}
/>
</div>
<button
ref={(node) => {
leftFloatingElemRef.current = node;
Expand Down
4 changes: 3 additions & 1 deletion packages/lexical-playground/src/ui/DropDown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ export default function DropDown({
buttonIconClassName,
children,
stopCloseOnClickSelf,
hideChevron,
}: {
disabled?: boolean;
buttonAriaLabel?: string;
Expand All @@ -174,6 +175,7 @@ export default function DropDown({
buttonLabel?: string;
children: ReactNode;
stopCloseOnClickSelf?: boolean;
hideChevron?: boolean;
}): JSX.Element {
const dropDownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
Expand Down Expand Up @@ -272,7 +274,7 @@ export default function DropDown({
{buttonLabel && (
<span className="text dropdown-button-text">{buttonLabel}</span>
)}
<i className="chevron-down" />
{!hideChevron && <i className="chevron-down" />}
</button>

{showDropDown &&
Expand Down