diff --git a/app/soapbox/features/compose/editor/index.tsx b/app/soapbox/features/compose/editor/index.tsx index 07b3c457b..6300ddad1 100644 --- a/app/soapbox/features/compose/editor/index.tsx +++ b/app/soapbox/features/compose/editor/index.tsx @@ -14,16 +14,18 @@ import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin'; import { ListPlugin } from '@lexical/react/LexicalListPlugin'; import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'; import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; +import { TablePlugin } from '@lexical/react/LexicalTablePlugin'; import clsx from 'clsx'; import { $createParagraphNode, $createTextNode, $getRoot } from 'lexical'; import { $createRemarkExport, $createRemarkImport } from 'lexical-remark'; import React, { useMemo, useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useAppDispatch, useFeatures } from 'soapbox/hooks'; +import { useAppDispatch, useFeatures, useInstance } from 'soapbox/hooks'; import { importImage } from './handlers/image'; import { useNodes } from './nodes'; +import TableCellNodes from './nodes/table-cell-nodes'; import AutosuggestPlugin from './plugins/autosuggest-plugin'; import FloatingBlockTypeToolbarPlugin from './plugins/floating-block-type-toolbar-plugin'; import FloatingLinkEditorPlugin from './plugins/floating-link-editor-plugin'; @@ -31,6 +33,8 @@ import FloatingTextFormatToolbarPlugin from './plugins/floating-text-format-tool import FocusPlugin from './plugins/focus-plugin'; import MentionPlugin from './plugins/mention-plugin'; import StatePlugin from './plugins/state-plugin'; +import TableActionMenuPlugin from './plugins/table-action-menu-plugin'; +import { TablePlugin as NewTablePlugin } from './plugins/table-plugin'; const LINK_MATCHERS = [ createLinkMatcherWithRegExp( @@ -53,6 +57,24 @@ interface IComposeEditor { placeholder?: JSX.Element | string } +const theme = { + hashtag: 'hover:underline text-primary-600 dark:text-accent-blue hover:text-primary-800 dark:hover:text-accent-blue', + mention: 'hover:underline text-primary-600 dark:text-accent-blue hover:text-primary-800 dark:hover:text-accent-blue', + text: { + bold: 'font-bold', + code: 'font-mono', + italic: 'italic', + strikethrough: 'line-through', + underline: 'underline', + underlineStrikethrough: 'underline-line-through', + }, + heading: { + h1: 'text-2xl font-bold', + h2: 'text-xl font-bold', + h3: 'text-lg font-semibold', + }, +}; + const ComposeEditor = React.forwardRef(({ className, placeholderClassName, @@ -69,6 +91,9 @@ const ComposeEditor = React.forwardRef(({ const dispatch = useAppDispatch(); const features = useFeatures(); const nodes = useNodes(); + const instance = useInstance(); + + const allowInlineTables = !!instance.pleroma.getIn(['metadata', 'markup', 'allow_inline_tables']); const [suggestionsHidden, setSuggestionsHidden] = useState(true); @@ -76,23 +101,7 @@ const ComposeEditor = React.forwardRef(({ namespace: 'ComposeForm', onError: console.error, nodes, - theme: { - hashtag: 'hover:underline text-primary-600 dark:text-accent-blue hover:text-primary-800 dark:hover:text-accent-blue', - mention: 'hover:underline text-primary-600 dark:text-accent-blue hover:text-primary-800 dark:hover:text-accent-blue', - text: { - bold: 'font-bold', - code: 'font-mono', - italic: 'italic', - strikethrough: 'line-through', - underline: 'underline', - underlineStrikethrough: 'underline-line-through', - }, - heading: { - h1: 'text-2xl font-bold', - h2: 'text-xl font-bold', - h3: 'text-lg font-semibold', - }, - }, + theme, editorState: dispatch((_, getState) => { const state = getState(); const compose = state.compose.get(composeId); @@ -124,6 +133,15 @@ const ComposeEditor = React.forwardRef(({ }), }), []); + const cellEditorConfig = useMemo(() => ({ + namespace: 'ComposeForm', + nodes: TableCellNodes, + onError: (error: Error) => { + throw error; + }, + theme, + }), []); + const [floatingAnchorElem, setFloatingAnchorElem] = useState(null); @@ -181,6 +199,20 @@ const ComposeEditor = React.forwardRef(({ + {allowInlineTables && } + {allowInlineTables && ( + + } + placeholder={null} + ErrorBoundary={LexicalErrorBoundary} + /> + + + + + )} + {allowInlineTables && } {features.richText && } diff --git a/app/soapbox/features/compose/editor/nodes/index.ts b/app/soapbox/features/compose/editor/nodes/index.ts index 0357504d9..430a84b46 100644 --- a/app/soapbox/features/compose/editor/nodes/index.ts +++ b/app/soapbox/features/compose/editor/nodes/index.ts @@ -10,12 +10,14 @@ import { AutoLinkNode, LinkNode } from '@lexical/link'; import { ListItemNode, ListNode } from '@lexical/list'; import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode'; import { HeadingNode, QuoteNode } from '@lexical/rich-text'; +import { TableCellNode, TableNode, TableRowNode } from '@lexical/table'; import { useFeatures, useInstance } from 'soapbox/hooks'; import { EmojiNode } from './emoji-node'; import { ImageNode } from './image-node'; import { MentionNode } from './mention-node'; +import { TableNode as NewTableNode } from './table-node'; import type { Klass, LexicalNode } from 'lexical'; @@ -32,18 +34,26 @@ const useNodes = () => { if (features.richText) { nodes.push( - QuoteNode, - CodeNode, CodeHighlightNode, + CodeNode, + HorizontalRuleNode, LinkNode, ListItemNode, ListNode, - HorizontalRuleNode, + QuoteNode, ); } if (instance.pleroma.getIn(['metadata', 'markup', 'allow_headings'])) nodes.push(HeadingNode); if (instance.pleroma.getIn(['metadata', 'markup', 'allow_inline_images'])) nodes.push(ImageNode); + if (instance.pleroma.getIn(['metadata', 'markup', 'allow_inline_tables'])) { + nodes.push( + NewTableNode, + TableCellNode, + TableNode, + TableRowNode, + ); + } return nodes; }; diff --git a/app/soapbox/features/compose/editor/nodes/table-cell-nodes.ts b/app/soapbox/features/compose/editor/nodes/table-cell-nodes.ts new file mode 100644 index 000000000..abc758448 --- /dev/null +++ b/app/soapbox/features/compose/editor/nodes/table-cell-nodes.ts @@ -0,0 +1,31 @@ +/** + * This source code is derived from code from Meta Platforms, Inc. + * and affiliates, licensed under the MIT license located in the + * LICENSE file in the /app/soapbox/features/compose/editor directory. + */ + +import { CodeHighlightNode, CodeNode } from '@lexical/code'; +import { HashtagNode } from '@lexical/hashtag'; +import { AutoLinkNode, LinkNode } from '@lexical/link'; +import { ListItemNode, ListNode } from '@lexical/list'; +import { QuoteNode } from '@lexical/rich-text'; + +import { EmojiNode } from './emoji-node'; +import { MentionNode } from './mention-node'; + +import type { Klass, LexicalNode } from 'lexical'; + +const TableCellNodes: Array> = [ + AutoLinkNode, + HashtagNode, + EmojiNode, + MentionNode, + QuoteNode, + CodeNode, + CodeHighlightNode, + LinkNode, + ListItemNode, + ListNode, +]; + +export default TableCellNodes; diff --git a/app/soapbox/features/compose/editor/nodes/table-component.tsx b/app/soapbox/features/compose/editor/nodes/table-component.tsx new file mode 100644 index 000000000..8cc97e63d --- /dev/null +++ b/app/soapbox/features/compose/editor/nodes/table-component.tsx @@ -0,0 +1,1781 @@ +/** + * This source code is derived from code from Meta Platforms, Inc. + * and affiliates, licensed under the MIT license located in the + * LICENSE file in the /app/soapbox/features/compose/editor directory. + */ + +import { + $generateJSONFromSelectedNodes, + $generateNodesFromSerializedNodes, + $insertGeneratedNodes, +} from '@lexical/clipboard'; +import { $generateHtmlFromNodes, $generateNodesFromDOM } from '@lexical/html'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { LexicalNestedComposer } from '@lexical/react/LexicalNestedComposer'; +import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection'; +import { mergeRegister } from '@lexical/utils'; +import { + $addUpdateTag, + $createParagraphNode, + $createRangeSelection, + $getNodeByKey, + $getRoot, + $getSelection, + $isNodeSelection, + $isRangeSelection, + CLICK_COMMAND, + COMMAND_PRIORITY_LOW, + COPY_COMMAND, + createEditor, + CUT_COMMAND, + EditorThemeClasses, + FORMAT_TEXT_COMMAND, + KEY_ARROW_DOWN_COMMAND, + KEY_ARROW_LEFT_COMMAND, + KEY_ARROW_RIGHT_COMMAND, + KEY_ARROW_UP_COMMAND, + KEY_BACKSPACE_COMMAND, + KEY_DELETE_COMMAND, + KEY_ENTER_COMMAND, + KEY_ESCAPE_COMMAND, + KEY_TAB_COMMAND, + LexicalEditor, + NodeKey, + PASTE_COMMAND, +} from 'lexical'; +import { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import * as React from 'react'; +import { createPortal } from 'react-dom'; + +import { CellContext } from '../plugins/table-plugin'; + +import { + $isTableNode, + Cell, + cellHTMLCache, + cellTextContentCache, + createRow, + createUID, + exportTableCellsToHTML, + extractRowsFromHTML, + Rows, + TableNode, +} from './table-node'; + +import type { RangeSelection, TextFormatType } from 'lexical'; + +type SortOptions = {type: 'ascending' | 'descending', x: number}; + +const NO_CELLS: [] = []; + +function $createSelectAll(): RangeSelection { + const sel = $createRangeSelection(); + sel.focus.set('root', $getRoot().getChildrenSize(), 'element'); + return sel; +} + +function createEmptyParagraphHTML(theme: EditorThemeClasses): string { + return `


`; +} + +function focusCell(tableElem: HTMLElement, id: string): void { + const cellElem = tableElem.querySelector(`[data-id=${id}]`) as HTMLElement; + if (!cellElem) { + return; + } + cellElem.focus(); +} + +function isStartingResize(target: HTMLElement): boolean { + return target.nodeType === 1 && target.hasAttribute('data-table-resize'); +} + +function generateHTMLFromJSON( + editorStateJSON: string, + cellEditor: LexicalEditor, +): string { + const editorState = cellEditor.parseEditorState(editorStateJSON); + let html = cellHTMLCache.get(editorStateJSON); + if (html === undefined) { + html = editorState.read(() => $generateHtmlFromNodes(cellEditor, null)); + const textContent = editorState.read(() => $getRoot().getTextContent()); + cellHTMLCache.set(editorStateJSON, html); + cellTextContentCache.set(editorStateJSON, textContent); + } + return html; +} + +function getCurrentDocument(editor: LexicalEditor): Document { + const rootElement = editor.getRootElement(); + return rootElement !== null ? rootElement.ownerDocument : document; +} + +function isCopy( + keyCode: number, + shiftKey: boolean, + metaKey: boolean, + ctrlKey: boolean, +): boolean { + if (shiftKey) { + return false; + } + if (keyCode === 67) { + return metaKey || ctrlKey; + } + + return false; +} + +function isCut( + keyCode: number, + shiftKey: boolean, + metaKey: boolean, + ctrlKey: boolean, +): boolean { + if (shiftKey) { + return false; + } + if (keyCode === 88) { + return metaKey || ctrlKey; + } + + return false; +} + +function isPaste( + keyCode: number, + shiftKey: boolean, + metaKey: boolean, + ctrlKey: boolean, +): boolean { + if (shiftKey) { + return false; + } + if (keyCode === 86) { + return metaKey || ctrlKey; + } + + return false; +} + +function getCellID(domElement: HTMLElement): null | string { + let node: null | HTMLElement = domElement; + while (node !== null) { + const possibleID = node.getAttribute('data-id'); + if (possibleID) { + return possibleID; + } + node = node.parentElement; + } + return null; +} + +function getTableCellWidth(domElement: HTMLElement): number { + let node: null | HTMLElement = domElement; + while (node !== null) { + if (node.nodeName === 'TH' || node.nodeName === 'TD') { + return node.getBoundingClientRect().width; + } + node = node.parentElement; + } + return 0; +} + +function $updateCells( + rows: Rows, + ids: Array, + cellCoordMap: Map, + cellEditor: null | LexicalEditor, + updateTableNode: (fn2: (tableNode: TableNode) => void) => void, + fn: () => void, +): void { + for (const id of ids) { + const cell = getCell(rows, id, cellCoordMap); + if (cell !== null && cellEditor !== null) { + const editorState = cellEditor.parseEditorState(cell.json); + cellEditor._headless = true; + cellEditor.setEditorState(editorState); + cellEditor.update(fn, { discrete: true }); + cellEditor._headless = false; + const newJSON = JSON.stringify(cellEditor.getEditorState()); + updateTableNode((tableNode) => { + const [x, y] = cellCoordMap.get(id) as [number, number]; + $addUpdateTag('history-push'); + tableNode.updateCellJSON(x, y, newJSON); + }); + } + } +} + +function isTargetOnPossibleUIControl(target: HTMLElement): boolean { + let node: HTMLElement | null = target; + while (node !== null) { + const nodeName = node.nodeName; + if ( + nodeName === 'BUTTON' || + nodeName === 'INPUT' || + nodeName === 'TEXTAREA' + ) { + return true; + } + node = node.parentElement; + } + return false; +} + +function getSelectedRect( + startID: string, + endID: string, + cellCoordMap: Map, +): null | {startX: number, endX: number, startY: number, endY: number} { + const startCoords = cellCoordMap.get(startID); + const endCoords = cellCoordMap.get(endID); + if (startCoords === undefined || endCoords === undefined) { + return null; + } + const startX = Math.min(startCoords[0], endCoords[0]); + const endX = Math.max(startCoords[0], endCoords[0]); + const startY = Math.min(startCoords[1], endCoords[1]); + const endY = Math.max(startCoords[1], endCoords[1]); + + return { + endX, + endY, + startX, + startY, + }; +} + +function getSelectedIDs( + rows: Rows, + startID: string, + endID: string, + cellCoordMap: Map, +): Array { + const rect = getSelectedRect(startID, endID, cellCoordMap); + if (rect === null) { + return []; + } + const { startX, endY, endX, startY } = rect; + const ids = []; + + for (let x = startX; x <= endX; x++) { + for (let y = startY; y <= endY; y++) { + ids.push(rows[y].cells[x].id); + } + } + return ids; +} + +function extractCellsFromRows( + rows: Rows, + rect: {startX: number, endX: number, startY: number, endY: number}, +): Rows { + const { startX, endY, endX, startY } = rect; + const newRows: Rows = []; + + for (let y = startY; y <= endY; y++) { + const row = rows[y]; + const newRow = createRow(); + for (let x = startX; x <= endX; x++) { + const cellClone = { ...row.cells[x] }; + cellClone.id = createUID(); + newRow.cells.push(cellClone); + } + newRows.push(newRow); + } + return newRows; +} + +function TableCellEditor({ cellEditor }: {cellEditor: LexicalEditor}) { + const { cellEditorConfig, cellEditorPlugins } = useContext(CellContext); + + if (cellEditorPlugins === null || cellEditorConfig === null) { + return null; + } + + return ( + + {cellEditorPlugins} + + ); +} + +function getCell( + rows: Rows, + cellID: string, + cellCoordMap: Map, +): null | Cell { + const coords = cellCoordMap.get(cellID); + if (coords === undefined) { + return null; + } + const [x, y] = coords; + const row = rows[y]; + return row.cells[x]; +} + +function TableActionMenu({ + cell, + rows, + cellCoordMap, + menuElem, + updateCellsByID, + onClose, + updateTableNode, + setSortingOptions, + sortingOptions, +}: { + cell: Cell + menuElem: HTMLElement + updateCellsByID: (ids: Array, fn: () => void) => void + onClose: () => void + updateTableNode: (fn2: (tableNode: TableNode) => void) => void + cellCoordMap: Map + rows: Rows + setSortingOptions: (options: null | SortOptions) => void + sortingOptions: null | SortOptions +}) { + const dropDownRef = useRef(null); + + useEffect(() => { + const dropdownElem = dropDownRef.current; + if (dropdownElem !== null) { + const rect = menuElem.getBoundingClientRect(); + dropdownElem.style.top = `${rect.y}px`; + dropdownElem.style.left = `${rect.x}px`; + } + }, [menuElem]); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const dropdownElem = dropDownRef.current; + if ( + dropdownElem !== null && + !dropdownElem.contains(event.target as Node) + ) { + event.stopPropagation(); + } + }; + + window.addEventListener('click', handleClickOutside); + return () => window.removeEventListener('click', handleClickOutside); + }, [onClose]); + const coords = cellCoordMap.get(cell.id); + + if (coords === undefined) { + return null; + } + const [x, y] = coords; + + return ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions +
{ + e.stopPropagation(); + }} + onPointerDown={(e) => { + e.stopPropagation(); + }} + onPointerUp={(e) => { + e.stopPropagation(); + }} + onClick={(e) => { + e.stopPropagation(); + }} + > + + +
+ {cell.type === 'header' && y === 0 && ( + <> + {sortingOptions !== null && sortingOptions.x === x && ( + + )} + {(sortingOptions === null || + sortingOptions.x !== x || + sortingOptions.type === 'descending') && ( + + )} + {(sortingOptions === null || + sortingOptions.x !== x || + sortingOptions.type === 'ascending') && ( + + )} +
+ + )} + + +
+ + +
+ {rows[0].cells.length !== 1 && ( + + )} + {rows.length !== 1 && ( + + )} + +
+ ); +} + +function TableCell({ + cell, + cellCoordMap, + cellEditor, + isEditing, + isSelected, + isPrimarySelected, + theme, + updateCellsByID, + updateTableNode, + rows, + setSortingOptions, + sortingOptions, +}: { + cell: Cell + isEditing: boolean + isSelected: boolean + isPrimarySelected: boolean + theme: EditorThemeClasses + cellEditor: LexicalEditor + updateCellsByID: (ids: Array, fn: () => void) => void + updateTableNode: (fn2: (tableNode: TableNode) => void) => void + cellCoordMap: Map + rows: Rows + setSortingOptions: (options: null | SortOptions) => void + sortingOptions: null | SortOptions +}) { + const [showMenu, setShowMenu] = useState(false); + const menuRootRef = useRef(null); + const isHeader = cell.type !== 'normal'; + const editorStateJSON = cell.json; + const CellComponent = isHeader ? 'th' : 'td'; + const cellWidth = cell.width; + const menuElem = menuRootRef.current; + const coords = cellCoordMap.get(cell.id); + const isSorted = + sortingOptions !== null && + coords !== undefined && + coords[0] === sortingOptions.x && + coords[1] === 0; + + useEffect(() => { + if (isEditing || !isPrimarySelected) { + setShowMenu(false); + } + }, [isEditing, isPrimarySelected]); + + return ( + + {isPrimarySelected && ( +
+ )} + {isPrimarySelected && isEditing ? ( + + ) : ( + <> +
+
+ + )} + {isPrimarySelected && !isEditing && ( +
+ +
+ )} + {showMenu && + menuElem !== null && + createPortal( + setShowMenu(false)} + updateTableNode={updateTableNode} + cellCoordMap={cellCoordMap} + rows={rows} + setSortingOptions={setSortingOptions} + sortingOptions={sortingOptions} + />, + document.body, + )} + {isSorted &&
} + + ); +} + +export default function TableComponent({ + nodeKey, + rows: rawRows, + theme, +}: { + nodeKey: NodeKey + rows: Rows + theme: EditorThemeClasses +}) { + const [isSelected, setSelected, clearSelection] = + useLexicalNodeSelection(nodeKey); + const resizeMeasureRef = useRef<{size: number, point: number}>({ + point: 0, + size: 0, + }); + const [sortingOptions, setSortingOptions] = useState( + null, + ); + const addRowsRef = useRef(null); + const lastCellIDRef = useRef(null); + const tableResizerRulerRef = useRef(null); + const { cellEditorConfig } = useContext(CellContext); + const [isEditing, setIsEditing] = useState(false); + const [showAddColumns, setShowAddColumns] = useState(false); + const [showAddRows, setShowAddRows] = useState(false); + const [editor] = useLexicalComposerContext(); + const mouseDownRef = useRef(false); + const [resizingID, setResizingID] = useState(null); + const tableRef = useRef(null); + const cellCoordMap = useMemo(() => { + const map = new Map(); + + for (let y = 0; y < rawRows.length; y++) { + const row = rawRows[y]; + const cells = row.cells; + for (let x = 0; x < cells.length; x++) { + const cell = cells[x]; + map.set(cell.id, [x, y]); + } + } + return map; + }, [rawRows]); + const rows = useMemo(() => { + if (sortingOptions === null) { + return rawRows; + } + const _rows = rawRows.slice(1); + _rows.sort((a, b) => { + const aCells = a.cells; + const bCells = b.cells; + const x = sortingOptions.x; + const aContent = cellTextContentCache.get(aCells[x].json) || ''; + const bContent = cellTextContentCache.get(bCells[x].json) || ''; + if (aContent === '' || bContent === '') { + return 1; + } + if (sortingOptions.type === 'ascending') { + return aContent.localeCompare(bContent); + } + return bContent.localeCompare(aContent); + }); + _rows.unshift(rawRows[0]); + return _rows; + }, [rawRows, sortingOptions]); + const [primarySelectedCellID, setPrimarySelectedCellID] = useState< + null | string + >(null); + const cellEditor = useMemo(() => { + if (cellEditorConfig === null) { + return null; + } + const _cellEditor = createEditor({ + namespace: cellEditorConfig.namespace, + nodes: cellEditorConfig.nodes, + onError: (error) => cellEditorConfig.onError(error, _cellEditor), + theme: cellEditorConfig.theme, + }); + return _cellEditor; + }, [cellEditorConfig]); + const [selectedCellIDs, setSelectedCellIDs] = useState>([]); + const selectedCellSet = useMemo>( + () => new Set(selectedCellIDs), + [selectedCellIDs], + ); + + useEffect(() => { + const tableElem = tableRef.current; + if ( + isSelected && + document.activeElement === document.body && + tableElem !== null + ) { + tableElem.focus(); + } + }, [isSelected]); + + const updateTableNode = useCallback( + (fn: (tableNode: TableNode) => void) => { + editor.update(() => { + const tableNode = $getNodeByKey(nodeKey); + if ($isTableNode(tableNode)) { + fn(tableNode); + } + }); + }, + [editor, nodeKey], + ); + + const addColumns = () => { + updateTableNode((tableNode) => { + $addUpdateTag('history-push'); + tableNode.addColumns(1); + }); + }; + + const addRows = () => { + updateTableNode((tableNode) => { + $addUpdateTag('history-push'); + tableNode.addRows(1); + }); + }; + + const modifySelectedCells = useCallback( + (x: number, y: number, extend: boolean) => { + const id = rows[y].cells[x].id; + lastCellIDRef.current = id; + if (extend) { + const selectedIDs = getSelectedIDs( + rows, + primarySelectedCellID as string, + id, + cellCoordMap, + ); + setSelectedCellIDs(selectedIDs); + } else { + setPrimarySelectedCellID(id); + setSelectedCellIDs(NO_CELLS); + focusCell(tableRef.current as HTMLElement, id); + } + }, + [cellCoordMap, primarySelectedCellID, rows], + ); + + const saveEditorToJSON = useCallback(() => { + if (cellEditor !== null && primarySelectedCellID !== null) { + const json = JSON.stringify(cellEditor.getEditorState()); + updateTableNode((tableNode) => { + const coords = cellCoordMap.get(primarySelectedCellID); + if (coords === undefined) { + return; + } + $addUpdateTag('history-push'); + const [x, y] = coords; + tableNode.updateCellJSON(x, y, json); + }); + } + }, [cellCoordMap, cellEditor, primarySelectedCellID, updateTableNode]); + + const selectTable = useCallback(() => { + setTimeout(() => { + const parentRootElement = editor.getRootElement(); + if (parentRootElement !== null) { + parentRootElement.focus({ preventScroll: true }); + window.getSelection()?.removeAllRanges(); + } + }, 20); + }, [editor]); + + useEffect(() => { + const tableElem = tableRef.current; + if (tableElem === null) { + return; + } + const doc = getCurrentDocument(editor); + + const isAtEdgeOfTable = (event: PointerEvent) => { + const x = event.clientX - tableRect.x; + const y = event.clientY - tableRect.y; + return x < 5 || y < 5; + }; + + const handlePointerDown = (event: PointerEvent) => { + const possibleID = getCellID(event.target as HTMLElement); + if ( + possibleID !== null && + editor.isEditable() && + tableElem.contains(event.target as HTMLElement) + ) { + if (isAtEdgeOfTable(event)) { + setSelected(true); + setPrimarySelectedCellID(null); + selectTable(); + return; + } + setSelected(false); + if (isStartingResize(event.target as HTMLElement)) { + setResizingID(possibleID); + tableElem.style.userSelect = 'none'; + resizeMeasureRef.current = { + point: event.clientX, + size: getTableCellWidth(event.target as HTMLElement), + }; + return; + } + mouseDownRef.current = true; + if (primarySelectedCellID !== possibleID) { + if (isEditing) { + saveEditorToJSON(); + } + setPrimarySelectedCellID(possibleID); + setIsEditing(false); + lastCellIDRef.current = possibleID; + } else { + lastCellIDRef.current = null; + } + setSelectedCellIDs(NO_CELLS); + } else if ( + primarySelectedCellID !== null && + !isTargetOnPossibleUIControl(event.target as HTMLElement) + ) { + setSelected(false); + mouseDownRef.current = false; + if (isEditing) { + saveEditorToJSON(); + } + setPrimarySelectedCellID(null); + setSelectedCellIDs(NO_CELLS); + setIsEditing(false); + lastCellIDRef.current = null; + } + }; + + const tableRect = tableElem.getBoundingClientRect(); + + const handlePointerMove = (event: PointerEvent) => { + if (resizingID !== null) { + const tableResizerRulerElem = tableResizerRulerRef.current; + if (tableResizerRulerElem !== null) { + const { size, point } = resizeMeasureRef.current; + const diff = event.clientX - point; + const newWidth = size + diff; + let x = event.clientX - tableRect.x; + if (x < 10) { + x = 10; + } else if (x > tableRect.width - 10) { + x = tableRect.width - 10; + } else if (newWidth < 20) { + x = point - size + 20 - tableRect.x; + } + tableResizerRulerElem.style.left = `${x}px`; + } + return; + } + if (!isEditing) { + const { clientX, clientY } = event; + const { width, x, y, height } = tableRect; + const isOnRightEdge = + clientX > x + width * 0.9 && + clientX < x + width + 40 && + !mouseDownRef.current; + setShowAddColumns(isOnRightEdge); + const isOnBottomEdge = + event.target === addRowsRef.current || + (clientY > y + height * 0.85 && + clientY < y + height + 5 && + !mouseDownRef.current); + setShowAddRows(isOnBottomEdge); + } + if ( + isEditing || + !mouseDownRef.current || + primarySelectedCellID === null + ) { + return; + } + const possibleID = getCellID(event.target as HTMLElement); + if (possibleID !== null && possibleID !== lastCellIDRef.current) { + if (selectedCellIDs.length === 0) { + tableElem.style.userSelect = 'none'; + } + const selectedIDs = getSelectedIDs( + rows, + primarySelectedCellID, + possibleID, + cellCoordMap, + ); + if (selectedIDs.length === 1) { + setSelectedCellIDs(NO_CELLS); + } else { + setSelectedCellIDs(selectedIDs); + } + lastCellIDRef.current = possibleID; + } + }; + + const handlePointerUp = (event: PointerEvent) => { + if (resizingID !== null) { + const { size, point } = resizeMeasureRef.current; + const diff = event.clientX - point; + let newWidth = size + diff; + if (newWidth < 10) { + newWidth = 10; + } + updateTableNode((tableNode) => { + const [x] = cellCoordMap.get(resizingID) as [number, number]; + $addUpdateTag('history-push'); + tableNode.updateColumnWidth(x, newWidth); + }); + setResizingID(null); + } + if ( + tableElem !== null && + selectedCellIDs.length > 1 && + mouseDownRef.current + ) { + tableElem.style.userSelect = 'text'; + window.getSelection()?.removeAllRanges(); + } + mouseDownRef.current = false; + }; + + doc.addEventListener('pointerdown', handlePointerDown); + doc.addEventListener('pointermove', handlePointerMove); + doc.addEventListener('pointerup', handlePointerUp); + + return () => { + doc.removeEventListener('pointerdown', handlePointerDown); + doc.removeEventListener('pointermove', handlePointerMove); + doc.removeEventListener('pointerup', handlePointerUp); + }; + }, [ + cellEditor, + editor, + isEditing, + rows, + saveEditorToJSON, + primarySelectedCellID, + selectedCellSet, + selectedCellIDs, + cellCoordMap, + resizingID, + updateTableNode, + setSelected, + selectTable, + ]); + + useEffect(() => { + if (!isEditing && primarySelectedCellID !== null) { + const doc = getCurrentDocument(editor); + + const loadContentIntoCell = (cell: Cell | null) => { + if (cell !== null && cellEditor !== null) { + const editorStateJSON = cell.json; + const editorState = cellEditor.parseEditorState(editorStateJSON); + cellEditor.setEditorState(editorState); + } + }; + + const handleDblClick = (event: MouseEvent) => { + const possibleID = getCellID(event.target as HTMLElement); + if (possibleID === primarySelectedCellID && editor.isEditable()) { + const cell = getCell(rows, possibleID, cellCoordMap); + loadContentIntoCell(cell); + setIsEditing(true); + setSelectedCellIDs(NO_CELLS); + } + }; + + const handleKeyDown = (event: KeyboardEvent) => { + // Ignore arrow keys, escape or tab + const keyCode = event.keyCode; + if ( + keyCode === 16 || + keyCode === 27 || + keyCode === 9 || + keyCode === 37 || + keyCode === 38 || + keyCode === 39 || + keyCode === 40 || + keyCode === 8 || + keyCode === 46 || + !editor.isEditable() + ) { + return; + } + if (keyCode === 13) { + event.preventDefault(); + } + if ( + !isEditing && + primarySelectedCellID !== null && + editor.getEditorState().read(() => $getSelection() === null) && + (event.target as HTMLElement).contentEditable !== 'true' + ) { + if (isCopy(keyCode, event.shiftKey, event.metaKey, event.ctrlKey)) { + editor.dispatchCommand(COPY_COMMAND, event); + return; + } + if (isCut(keyCode, event.shiftKey, event.metaKey, event.ctrlKey)) { + editor.dispatchCommand(CUT_COMMAND, event); + return; + } + if (isPaste(keyCode, event.shiftKey, event.metaKey, event.ctrlKey)) { + editor.dispatchCommand(PASTE_COMMAND, event); + return; + } + } + if (event.metaKey || event.ctrlKey || event.altKey) { + return; + } + const cell = getCell(rows, primarySelectedCellID, cellCoordMap); + loadContentIntoCell(cell); + setIsEditing(true); + setSelectedCellIDs(NO_CELLS); + }; + + doc.addEventListener('dblclick', handleDblClick); + doc.addEventListener('keydown', handleKeyDown); + + return () => { + doc.removeEventListener('dblclick', handleDblClick); + doc.removeEventListener('keydown', handleKeyDown); + }; + } + }, [ + cellEditor, + editor, + isEditing, + rows, + primarySelectedCellID, + cellCoordMap, + ]); + + const updateCellsByID = useCallback( + (ids: Array, fn: () => void) => { + $updateCells(rows, ids, cellCoordMap, cellEditor, updateTableNode, fn); + }, + [cellCoordMap, cellEditor, rows, updateTableNode], + ); + + const clearCellsCommand = useCallback((): boolean => { + if (primarySelectedCellID !== null && !isEditing) { + updateCellsByID([primarySelectedCellID, ...selectedCellIDs], () => { + const root = $getRoot(); + root.clear(); + root.append($createParagraphNode()); + }); + return true; + } else if (isSelected) { + updateTableNode((tableNode) => { + $addUpdateTag('history-push'); + tableNode.selectNext(); + tableNode.remove(); + }); + } + return false; + }, [ + isEditing, + isSelected, + primarySelectedCellID, + selectedCellIDs, + updateCellsByID, + updateTableNode, + ]); + + useEffect(() => { + const tableElem = tableRef.current; + if (tableElem === null) { + return; + } + + const copyDataToClipboard = ( + event: ClipboardEvent, + htmlString: string, + lexicalString: string, + plainTextString: string, + ) => { + const clipboardData = + event instanceof KeyboardEvent ? null : event.clipboardData; + event.preventDefault(); + + if (clipboardData) { + clipboardData.setData('text/html', htmlString); + clipboardData.setData('text/plain', plainTextString); + clipboardData.setData('application/x-lexical-editor', lexicalString); + } else { + const clipboard = navigator.clipboard; + if (clipboard) { + // Most browsers only support a single item in the clipboard at one time. + // So we optimize by only putting in HTML. + const data = [ + new ClipboardItem({ + 'text/html': new Blob([htmlString as BlobPart], { + type: 'text/html', + }), + }), + ]; + clipboard.write(data); + } + } + }; + + const getTypeFromObject = async ( + clipboardData: DataTransfer | ClipboardItem, + type: string, + ): Promise => { + try { + return clipboardData instanceof DataTransfer + ? clipboardData.getData(type) + : clipboardData instanceof ClipboardItem + ? await (await clipboardData.getType(type)).text() + : ''; + } catch { + return ''; + } + }; + + const pasteContent = async (event: ClipboardEvent) => { + let clipboardData: null | DataTransfer | ClipboardItem = + (event instanceof InputEvent ? null : event.clipboardData) || null; + + if (primarySelectedCellID !== null && cellEditor !== null) { + event.preventDefault(); + + if (clipboardData === null) { + try { + const items = await navigator.clipboard.read(); + clipboardData = items[0]; + } catch { + // NO-OP + } + } + const lexicalString = + clipboardData !== null + ? await getTypeFromObject( + clipboardData, + 'application/x-lexical-editor', + ) + : ''; + + if (lexicalString) { + try { + const payload = JSON.parse(lexicalString); + if ( + payload.namespace === editor._config.namespace && + Array.isArray(payload.nodes) + ) { + $updateCells( + rows, + [primarySelectedCellID], + cellCoordMap, + cellEditor, + updateTableNode, + () => { + const root = $getRoot(); + root.clear(); + root.append($createParagraphNode()); + root.selectEnd(); + const nodes = $generateNodesFromSerializedNodes( + payload.nodes, + ); + const sel = $getSelection(); + if ($isRangeSelection(sel)) { + $insertGeneratedNodes(cellEditor, nodes, sel); + } + }, + ); + return; + } + // eslint-disable-next-line no-empty + } catch {} + } + const htmlString = + clipboardData !== null + ? await getTypeFromObject(clipboardData, 'text/html') + : ''; + + if (htmlString) { + try { + const parser = new DOMParser(); + const dom = parser.parseFromString(htmlString, 'text/html'); + const possibleTableElement = dom.querySelector('table'); + + if (possibleTableElement) { + const pasteRows = extractRowsFromHTML(possibleTableElement); + updateTableNode((tableNode) => { + const [x, y] = cellCoordMap.get(primarySelectedCellID) as [ + number, + number, + ]; + $addUpdateTag('history-push'); + tableNode.mergeRows(x, y, pasteRows); + }); + return; + } + $updateCells( + rows, + [primarySelectedCellID], + cellCoordMap, + cellEditor, + updateTableNode, + () => { + const root = $getRoot(); + root.clear(); + root.append($createParagraphNode()); + root.selectEnd(); + const nodes = $generateNodesFromDOM(editor, dom); + const sel = $getSelection(); + if ($isRangeSelection(sel)) { + $insertGeneratedNodes(cellEditor, nodes, sel); + } + }, + ); + return; + // eslint-disable-next-line no-empty + } catch {} + } + + // Multi-line plain text in rich text mode pasted as separate paragraphs + // instead of single paragraph with linebreaks. + const text = + clipboardData !== null + ? await getTypeFromObject(clipboardData, 'text/plain') + : ''; + + if (text) { + $updateCells( + rows, + [primarySelectedCellID], + cellCoordMap, + cellEditor, + updateTableNode, + () => { + const root = $getRoot(); + root.clear(); + root.selectEnd(); + const sel = $getSelection(); + if (sel !== null) { + sel.insertRawText(text); + } + }, + ); + } + } + }; + + const copyPrimaryCell = (event: ClipboardEvent) => { + if (primarySelectedCellID !== null && cellEditor !== null) { + const cell = getCell(rows, primarySelectedCellID, cellCoordMap) as Cell; + const json = cell.json; + const htmlString = cellHTMLCache.get(json) || null; + if (htmlString === null) { + return; + } + const editorState = cellEditor.parseEditorState(json); + const plainTextString = editorState.read(() => + $getRoot().getTextContent(), + ); + const lexicalString = editorState.read(() => { + return JSON.stringify( + $generateJSONFromSelectedNodes(cellEditor, null), + ); + }); + + copyDataToClipboard(event, htmlString, lexicalString, plainTextString); + } + }; + + const copyCellRange = (event: ClipboardEvent) => { + const lastCellID = lastCellIDRef.current; + if ( + primarySelectedCellID !== null && + cellEditor !== null && + lastCellID !== null + ) { + const rect = getSelectedRect( + primarySelectedCellID, + lastCellID, + cellCoordMap, + ); + if (rect === null) { + return; + } + const dom = exportTableCellsToHTML(rows, rect); + const htmlString = dom.outerHTML; + const plainTextString = dom.outerText; + const tableNodeJSON = editor.getEditorState().read(() => { + const tableNode = $getNodeByKey(nodeKey) as TableNode; + return tableNode.exportJSON(); + }); + tableNodeJSON.rows = extractCellsFromRows(rows, rect); + const lexicalJSON = { + namespace: cellEditor._config.namespace, + nodes: [tableNodeJSON], + }; + const lexicalString = JSON.stringify(lexicalJSON); + copyDataToClipboard(event, htmlString, lexicalString, plainTextString); + } + }; + + const handlePaste = ( + event: ClipboardEvent, + activeEditor: LexicalEditor, + ) => { + const selection = $getSelection(); + if ( + primarySelectedCellID !== null && + !isEditing && + selection === null && + activeEditor === editor + ) { + pasteContent(event); + mouseDownRef.current = false; + setSelectedCellIDs(NO_CELLS); + return true; + } + return false; + }; + + const handleCopy = (event: ClipboardEvent, activeEditor: LexicalEditor) => { + const selection = $getSelection(); + if ( + primarySelectedCellID !== null && + !isEditing && + selection === null && + activeEditor === editor + ) { + if (selectedCellIDs.length === 0) { + copyPrimaryCell(event); + } else { + copyCellRange(event); + } + return true; + } + return false; + }; + + return mergeRegister( + editor.registerCommand( + CLICK_COMMAND, + (payload) => { + const selection = $getSelection(); + if ($isNodeSelection(selection)) { + return true; + } + return false; + }, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + PASTE_COMMAND, + handlePaste, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + COPY_COMMAND, + handleCopy, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + CUT_COMMAND, + (event: ClipboardEvent, activeEditor) => { + if (handleCopy(event, activeEditor)) { + clearCellsCommand(); + return true; + } + return false; + }, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + KEY_BACKSPACE_COMMAND, + clearCellsCommand, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + KEY_DELETE_COMMAND, + clearCellsCommand, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + FORMAT_TEXT_COMMAND, + (payload) => { + if (primarySelectedCellID !== null && !isEditing) { + $updateCells( + rows, + [primarySelectedCellID, ...selectedCellIDs], + cellCoordMap, + cellEditor, + updateTableNode, + () => { + const sel = $createSelectAll(); + sel.formatText(payload); + }, + ); + return true; + } + return false; + }, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + KEY_ENTER_COMMAND, + (event, targetEditor) => { + const selection = $getSelection(); + if ( + primarySelectedCellID === null && + !isEditing && + $isNodeSelection(selection) && + selection.has(nodeKey) && + selection.getNodes().length === 1 && + targetEditor === editor + ) { + const firstCellID = rows[0].cells[0].id; + setPrimarySelectedCellID(firstCellID); + focusCell(tableElem, firstCellID); + event.preventDefault(); + event.stopPropagation(); + clearSelection(); + return true; + } + return false; + }, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + KEY_TAB_COMMAND, + (event) => { + const selection = $getSelection(); + if ( + !isEditing && + selection === null && + primarySelectedCellID !== null + ) { + const isBackward = event.shiftKey; + const [x, y] = cellCoordMap.get(primarySelectedCellID) as [ + number, + number, + ]; + event.preventDefault(); + let nextX = null; + let nextY = null; + if (x === 0 && isBackward) { + if (y !== 0) { + nextY = y - 1; + nextX = rows[nextY].cells.length - 1; + } + } else if (x === rows[y].cells.length - 1 && !isBackward) { + if (y !== rows.length - 1) { + nextY = y + 1; + nextX = 0; + } + } else if (!isBackward) { + nextX = x + 1; + nextY = y; + } else { + nextX = x - 1; + nextY = y; + } + if (nextX !== null && nextY !== null) { + modifySelectedCells(nextX, nextY, false); + return true; + } + } + return false; + }, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + KEY_ARROW_UP_COMMAND, + (event, targetEditor) => { + const selection = $getSelection(); + if (!isEditing && selection === null) { + const extend = event.shiftKey; + const cellID = extend + ? lastCellIDRef.current || primarySelectedCellID + : primarySelectedCellID; + if (cellID !== null) { + const [x, y] = cellCoordMap.get(cellID) as [number, number]; + if (y !== 0) { + modifySelectedCells(x, y - 1, extend); + return true; + } + } + } + if (!$isRangeSelection(selection) || targetEditor !== cellEditor) { + return false; + } + if ( + selection.isCollapsed() && + selection.anchor + .getNode() + .getTopLevelElementOrThrow() + .getPreviousSibling() === null + ) { + event.preventDefault(); + return true; + } + return false; + }, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + KEY_ARROW_DOWN_COMMAND, + (event, targetEditor) => { + const selection = $getSelection(); + if (!isEditing && selection === null) { + const extend = event.shiftKey; + const cellID = extend + ? lastCellIDRef.current || primarySelectedCellID + : primarySelectedCellID; + if (cellID !== null) { + const [x, y] = cellCoordMap.get(cellID) as [number, number]; + if (y !== rows.length - 1) { + modifySelectedCells(x, y + 1, extend); + return true; + } + } + } + if (!$isRangeSelection(selection) || targetEditor !== cellEditor) { + return false; + } + if ( + selection.isCollapsed() && + selection.anchor + .getNode() + .getTopLevelElementOrThrow() + .getNextSibling() === null + ) { + event.preventDefault(); + return true; + } + return false; + }, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + KEY_ARROW_LEFT_COMMAND, + (event, targetEditor) => { + const selection = $getSelection(); + if (!isEditing && selection === null) { + const extend = event.shiftKey; + const cellID = extend + ? lastCellIDRef.current || primarySelectedCellID + : primarySelectedCellID; + if (cellID !== null) { + const [x, y] = cellCoordMap.get(cellID) as [number, number]; + if (x !== 0) { + modifySelectedCells(x - 1, y, extend); + return true; + } + } + } + if (!$isRangeSelection(selection) || targetEditor !== cellEditor) { + return false; + } + if (selection.isCollapsed() && selection.anchor.offset === 0) { + event.preventDefault(); + return true; + } + return false; + }, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + KEY_ARROW_RIGHT_COMMAND, + (event, targetEditor) => { + const selection = $getSelection(); + if (!isEditing && selection === null) { + const extend = event.shiftKey; + const cellID = extend + ? lastCellIDRef.current || primarySelectedCellID + : primarySelectedCellID; + if (cellID !== null) { + const [x, y] = cellCoordMap.get(cellID) as [number, number]; + if (x !== rows[y].cells.length - 1) { + modifySelectedCells(x + 1, y, extend); + return true; + } + } + } + if (!$isRangeSelection(selection) || targetEditor !== cellEditor) { + return false; + } + if (selection.isCollapsed()) { + const anchor = selection.anchor; + if ( + (anchor.type === 'text' && + anchor.offset === anchor.getNode().getTextContentSize()) || + (anchor.type === 'element' && + anchor.offset === anchor.getNode().getChildrenSize()) + ) { + event.preventDefault(); + return true; + } + } + return false; + }, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + KEY_ESCAPE_COMMAND, + (event, targetEditor) => { + const selection = $getSelection(); + if (!isEditing && selection === null && targetEditor === editor) { + setSelected(true); + setPrimarySelectedCellID(null); + selectTable(); + return true; + } + if (!$isRangeSelection(selection)) { + return false; + } + if (isEditing) { + saveEditorToJSON(); + setIsEditing(false); + if (primarySelectedCellID !== null) { + setTimeout(() => { + focusCell(tableElem, primarySelectedCellID); + }, 20); + } + return true; + } + return false; + }, + COMMAND_PRIORITY_LOW, + ), + ); + }, [ + cellCoordMap, + cellEditor, + clearCellsCommand, + clearSelection, + editor, + isEditing, + modifySelectedCells, + nodeKey, + primarySelectedCellID, + rows, + saveEditorToJSON, + selectTable, + selectedCellIDs, + setSelected, + updateTableNode, + ]); + + if (cellEditor === null) { + return; + } + + return ( +
+ + + {rows.map((row) => ( + + {row.cells.map((cell) => { + const { id } = cell; + return ( + + ); + })} + + ))} + +
+ {showAddColumns && ( + + ); + } else if (canUnmergeCell) { + mergeCellButton = ( + + ); + } + } + + return createPortal( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions +
{ + e.stopPropagation(); + }} + > + {mergeCellButton} +
+ + +
+ + +
+ + + +
+ + +
, + document.body, + ); +} + +function TableCellActionMenuContainer({ + anchorElem, + cellMerge, +}: { + anchorElem: HTMLElement + cellMerge: boolean +}): JSX.Element { + const [editor] = useLexicalComposerContext(); + + const menuButtonRef = useRef(null); + const menuRootRef = useRef(null); + const [isMenuOpen, setIsMenuOpen] = useState(false); + + const [tableCellNode, setTableMenuCellNode] = useState( + null, + ); + + const moveMenu = useCallback(() => { + const menu = menuButtonRef.current; + const selection = $getSelection(); + const nativeSelection = window.getSelection(); + const activeElement = document.activeElement; + + if (!selection || !menu) { + setTableMenuCellNode(null); + return; + } + + const rootElement = editor.getRootElement(); + + if ( + $isRangeSelection(selection) && + rootElement !== null && + nativeSelection !== null && + rootElement.contains(nativeSelection.anchorNode) + ) { + const tableCellNodeFromSelection = $getTableCellNodeFromLexicalNode( + selection.anchor.getNode(), + ); + + if (!tableCellNodeFromSelection) { + setTableMenuCellNode(null); + return; + } + + const tableCellParentNodeDOM = editor.getElementByKey( + tableCellNodeFromSelection.getKey(), + ); + + if (!tableCellParentNodeDOM) { + setTableMenuCellNode(null); + return; + } + + setTableMenuCellNode(tableCellNodeFromSelection); + } else if (!activeElement) { + setTableMenuCellNode(null); + } + }, [editor]); + + useEffect(() => { + return editor.registerUpdateListener(() => { + editor.getEditorState().read(() => { + moveMenu(); + }); + }); + }); + + useEffect(() => { + const menuButtonDOM = menuButtonRef.current as HTMLButtonElement | null; + + if (menuButtonDOM && tableCellNode) { + const tableCellNodeDOM = editor.getElementByKey(tableCellNode.getKey()); + + if (tableCellNodeDOM) { + const tableCellRect = tableCellNodeDOM.getBoundingClientRect(); + const menuRect = menuButtonDOM.getBoundingClientRect(); + const anchorRect = anchorElem.getBoundingClientRect(); + + const top = tableCellRect.top - anchorRect.top + 4; + const left = + tableCellRect.right - menuRect.width - 10 - anchorRect.left; + + menuButtonDOM.style.opacity = '1'; + menuButtonDOM.style.transform = `translate(${left}px, ${top}px)`; + } else { + menuButtonDOM.style.opacity = '0'; + menuButtonDOM.style.transform = 'translate(-10000px, -10000px)'; + } + } + }, [menuButtonRef, tableCellNode, editor, anchorElem]); + + const prevTableCellDOM = useRef(tableCellNode); + + useEffect(() => { + if (prevTableCellDOM.current !== tableCellNode) { + setIsMenuOpen(false); + } + + prevTableCellDOM.current = tableCellNode; + }, [prevTableCellDOM, tableCellNode]); + + return ( +
+ {tableCellNode && ( + <> + + {isMenuOpen && ( + setIsMenuOpen(false)} + tableCellNode={tableCellNode} + cellMerge={cellMerge} + /> + )} + + )} +
+ ); +} + +export default function TableActionMenuPlugin({ + anchorElem, + cellMerge = false, +}: { + anchorElem?: HTMLElement | null + cellMerge?: boolean +}): null | ReactPortal { + const isEditable = useLexicalEditable(); + return createPortal( + isEditable ? ( + + ) : null, + anchorElem || document.body, + ); +} diff --git a/app/soapbox/features/compose/editor/plugins/table-plugin.tsx b/app/soapbox/features/compose/editor/plugins/table-plugin.tsx new file mode 100644 index 000000000..7c2fdead3 --- /dev/null +++ b/app/soapbox/features/compose/editor/plugins/table-plugin.tsx @@ -0,0 +1,116 @@ +/** + * This source code is derived from code from Meta Platforms, Inc. + * and affiliates, licensed under the MIT license located in the + * LICENSE file in the /app/soapbox/features/compose/editor directory. + */ + +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { + $insertNodes, + COMMAND_PRIORITY_EDITOR, + createCommand, + EditorThemeClasses, + Klass, + LexicalCommand, + LexicalEditor, + LexicalNode, +} from 'lexical'; +import { createContext, useContext, useEffect, useMemo, useState } from 'react'; +import * as React from 'react'; + +import { $createTableNodeWithDimensions, TableNode } from '../nodes/table-node'; + +export type InsertTableCommandPayload = Readonly<{ + columns: number + rows: number + includeHeaders?: boolean +}>; + +export type CellContextShape = { + cellEditorConfig: null | CellEditorConfig + cellEditorPlugins: null | JSX.Element | Array + set: ( + cellEditorConfig: null | CellEditorConfig, + cellEditorPlugins: null | JSX.Element | Array, + ) => void +}; + +export type CellEditorConfig = Readonly<{ + namespace: string + nodes?: ReadonlyArray> + onError: (error: Error, editor: LexicalEditor) => void + readOnly?: boolean + theme?: EditorThemeClasses +}>; + +export const INSERT_NEW_TABLE_COMMAND: LexicalCommand = + createCommand('INSERT_NEW_TABLE_COMMAND'); + +export const CellContext = createContext({ + cellEditorConfig: null, + cellEditorPlugins: null, + set: () => { + // Empty + }, +}); + +export function TableContext({ children }: {children: JSX.Element}) { + const [contextValue, setContextValue] = useState<{ + cellEditorConfig: null | CellEditorConfig + cellEditorPlugins: null | JSX.Element | Array + }>({ + cellEditorConfig: null, + cellEditorPlugins: null, + }); + return ( + ({ + cellEditorConfig: contextValue.cellEditorConfig, + cellEditorPlugins: contextValue.cellEditorPlugins, + set: (cellEditorConfig, cellEditorPlugins) => { + setContextValue({ cellEditorConfig, cellEditorPlugins }); + }, + }), + [contextValue.cellEditorConfig, contextValue.cellEditorPlugins], + )} + > + {children} + + ); +} + +export function TablePlugin({ + cellEditorConfig, + children, +}: { + cellEditorConfig: CellEditorConfig + children: JSX.Element | Array +}): JSX.Element | null { + const [editor] = useLexicalComposerContext(); + const cellContext = useContext(CellContext); + + useEffect(() => { + if (!editor.hasNodes([TableNode])) { + throw new Error('TablePlugin: TableNode is not registered on editor'); + } + + cellContext.set(cellEditorConfig, children); + + return editor.registerCommand( + INSERT_NEW_TABLE_COMMAND, + ({ columns, rows, includeHeaders }) => { + const tableNode = $createTableNodeWithDimensions( + rows, + columns, + includeHeaders, + ); + $insertNodes([tableNode]); + return true; + }, + COMMAND_PRIORITY_EDITOR, + ); + }, [cellContext, cellEditorConfig, children, editor]); + + return null; +} diff --git a/package.json b/package.json index 3e2189bda..542db6f33 100644 --- a/package.json +++ b/package.json @@ -52,14 +52,17 @@ "@gamestdio/websocket": "^0.3.2", "@jest/globals": "^29.0.0", "@lcdp/offline-plugin": "^5.1.0", - "@lexical/code": "^0.11.2", - "@lexical/hashtag": "^0.11.2", - "@lexical/link": "^0.11.2", - "@lexical/list": "^0.11.2", - "@lexical/react": "^0.11.2", - "@lexical/rich-text": "^0.11.2", - "@lexical/selection": "^0.11.2", - "@lexical/utils": "^0.11.2", + "@lexical/clipboard": "^0.11.3", + "@lexical/code": "^0.11.3", + "@lexical/hashtag": "^0.11.3", + "@lexical/html": "^0.11.3", + "@lexical/link": "^0.11.3", + "@lexical/list": "^0.11.3", + "@lexical/react": "^0.11.3", + "@lexical/rich-text": "^0.11.3", + "@lexical/selection": "^0.11.3", + "@lexical/table": "^0.11.3", + "@lexical/utils": "^0.11.3", "@metamask/providers": "^10.0.0", "@popperjs/core": "^2.11.5", "@reach/combobox": "^0.18.0", @@ -134,7 +137,7 @@ "intl-messageformat-parser": "^6.0.0", "intl-pluralrules": "^1.3.1", "leaflet": "^1.8.0", - "lexical": "^0.11.2", + "lexical": "^0.11.3", "lexical-remark": "^0.3.8", "libphonenumber-js": "^1.10.8", "line-awesome": "^1.3.0", diff --git a/yarn.lock b/yarn.lock index 66347cafd..d6f14ecbf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2188,159 +2188,159 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== -"@lexical/clipboard@0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@lexical/clipboard/-/clipboard-0.11.2.tgz#586d02a4b1eb243cb44025809e8daa2f5e4a7bcc" - integrity sha512-Cq3chsBhX9fJPWQXxqoaNr+MSN9VMbplhQXwEQJOXHYErkAVQgpr4cSlPFVD+MI2MO1TMhYRkMlXWZ33iYmcKQ== +"@lexical/clipboard@0.11.3", "@lexical/clipboard@^0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@lexical/clipboard/-/clipboard-0.11.3.tgz#f4bfac8fad1f41d43a45c4e1ffc79542757314f9" + integrity sha512-6xggT8b0hd4OQy25mBH+yiJsr3Bm8APHjDOd3yINCGeiiHXIC+2qKQn3MG70euxQQuyzq++tYHcSsFq42g8Jyw== dependencies: - "@lexical/html" "0.11.2" - "@lexical/list" "0.11.2" - "@lexical/selection" "0.11.2" - "@lexical/utils" "0.11.2" + "@lexical/html" "0.11.3" + "@lexical/list" "0.11.3" + "@lexical/selection" "0.11.3" + "@lexical/utils" "0.11.3" -"@lexical/code@0.11.2", "@lexical/code@^0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@lexical/code/-/code-0.11.2.tgz#add0bfd8fe0c76b0bf764e33c501c83a3fc0cb2d" - integrity sha512-xR+K8qtvSeKefNIvRCbDT2iNdl+5UKxDmQLh1ZqUeT5TBefXevnF0Hevek8xs1f+XSYwNeXjK/x1THD/i9e2jQ== +"@lexical/code@0.11.3", "@lexical/code@^0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@lexical/code/-/code-0.11.3.tgz#4a5ef193655557859c63dd4a54012c78580585fa" + integrity sha512-BIMPd2op65iP4N9SkKIUVodZoWeSsnk6skNJ8UHBO/Rg0ZxyAqxLpnBhEgHq2QOoTBbEW6OEFtkc7/+f9LINZg== dependencies: - "@lexical/utils" "0.11.2" + "@lexical/utils" "0.11.3" prismjs "^1.27.0" -"@lexical/dragon@0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@lexical/dragon/-/dragon-0.11.2.tgz#9a06676c18edb03644f14a233ed63bcdae8c466f" - integrity sha512-zJXeUiViYEbOEL9aXSYZmzku7SytZbD2pQ1FYQJ6H2o7UoK1WkYunRZkyxrx6uclQnZTUB1VO/OM2KuYlzl9wA== +"@lexical/dragon@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@lexical/dragon/-/dragon-0.11.3.tgz#b4254953b09a68c20277ba0fb125548dd6630921" + integrity sha512-S18uwqOOpV2yIAFVWqSvBdhZ5BGadPQO4ejZF15wP8LUuqkxCs+0I/MjLovQ7tx0Cx34KdDaOXtM6XeG74ixYw== -"@lexical/hashtag@0.11.2", "@lexical/hashtag@^0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@lexical/hashtag/-/hashtag-0.11.2.tgz#266d0a8df48abbaac0ffa694b191ba25e71a5f20" - integrity sha512-nXtjIW8K+YLUBwh70vtO2oy5IpYiSb1XwM2Wn4c2yrzKk7tOVs7JAVO48Vyag0DsCpfLj1n+F2vd0zsLC147/A== +"@lexical/hashtag@0.11.3", "@lexical/hashtag@^0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@lexical/hashtag/-/hashtag-0.11.3.tgz#9ffb2f7b2bb9a62fa9641355fc06feb9d209273a" + integrity sha512-7auoaWp2QhsX9/Bq0SxLXatUaSwqoT9HlWNTH2vKsw8tdeUBYacTHLuBNncTGrznXLG0/B5+FWoLuM6Pzqq4Ig== dependencies: - "@lexical/utils" "0.11.2" + "@lexical/utils" "0.11.3" -"@lexical/history@0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@lexical/history/-/history-0.11.2.tgz#1bcb9627ee469e038d582e43a3027d1c2bf41ff0" - integrity sha512-N2oaLhCrCt6AkAIba+sEzhPo5VrjaZDC/2Arn7/2q8vDnQc5WqoyNtEtFaYp+or5oI93C1jM9Qp7FB54lobAXA== +"@lexical/history@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@lexical/history/-/history-0.11.3.tgz#bcf7b2e708a5b3b5f90d1a39e07e0ae51bac41df" + integrity sha512-QLJQRH2rbadRwXd4c/U4TqjLWDQna6Q43nCocIZF+SdVG9TlASp7m6dS7hiHfPtV1pkxJUxPhZY6EsB/Ok5WGA== dependencies: - "@lexical/utils" "0.11.2" + "@lexical/utils" "0.11.3" -"@lexical/html@0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@lexical/html/-/html-0.11.2.tgz#d92e8e965cf0032d1ff4f8a1c6942a07264de76a" - integrity sha512-QzPqci+FBoaOJcdqO2XXaiowUDVTkGN8y6ycZPjKYfu+fMDdkfSC6b3fdLfb99v6FluriVih39DJ8WQC0PcC1g== +"@lexical/html@0.11.3", "@lexical/html@^0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@lexical/html/-/html-0.11.3.tgz#c02b38f512eb808726922c8215dd2374df6b77cc" + integrity sha512-+8AYnxxml9PneZLkGfdTenqDjE2yD1ZfCmQLrD/L1TEn22OjZh4uvKVHb13wEhgUZTuLKF0PNdnuecko9ON/aQ== dependencies: - "@lexical/selection" "0.11.2" + "@lexical/selection" "0.11.3" -"@lexical/link@0.11.2", "@lexical/link@^0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@lexical/link/-/link-0.11.2.tgz#6807a62c76e9b8a53f1ac1630d958b56685e1756" - integrity sha512-/EYhUkWnQ7cjJDnNrl8SNCT8zNicryOIf8PIkRpDKnDgG3SQMzXV6NT6JRAebam913AenfcBLaje21zBul7m4w== +"@lexical/link@0.11.3", "@lexical/link@^0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@lexical/link/-/link-0.11.3.tgz#7efab94e3b061a84639314eaec0b915d3457329c" + integrity sha512-stAjIrDrF18dPKK25ExPwMCcMe0KKD0FWVzo3F7ejh9DvrQcLFeBPcs8ze71chS3D5fQDB/CzdwvMjEViKmq2A== dependencies: - "@lexical/utils" "0.11.2" + "@lexical/utils" "0.11.3" -"@lexical/list@0.11.2", "@lexical/list@^0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@lexical/list/-/list-0.11.2.tgz#983ba1a6065054ae34d44338a9d8a997ff854a2f" - integrity sha512-VK+CeJdWaZS/H2ivELxIZd8UTFAJuqkMMF1XzQqZDxYKus8TJWBsG2SwY3ouADpHifrBTufIZlQrLSwaqxrACA== +"@lexical/list@0.11.3", "@lexical/list@^0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@lexical/list/-/list-0.11.3.tgz#d158ac4b4b42d772b30a1cd2a42ca0462a5a154e" + integrity sha512-Cs9071wDfqi4j1VgodceiR1jTHj13eCoEJDhr3e/FW0x5we7vfbTMtWlOWbveIoryAh+rQNgiD5e8SrAm6Zs3g== dependencies: - "@lexical/utils" "0.11.2" + "@lexical/utils" "0.11.3" -"@lexical/mark@0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@lexical/mark/-/mark-0.11.2.tgz#93df8a2e3a0d088cfc3618c4e44b5417b24ac96a" - integrity sha512-OtOjQmsQNraTR7TQhMs6sUCAyT+903EZbP48BLqwIBEZNyRK8FsBh8boI68sdTBpypUmHfYpzn1aAJnLDocTTQ== +"@lexical/mark@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@lexical/mark/-/mark-0.11.3.tgz#7f87a264d44762e275ba7b3d85e18307bbabd9e3" + integrity sha512-0wAtufmaA0rMVFXoiJ0sY/tiJsQbHuDpgywb1Qa8qnZZcg7ZTrQMz9Go0fEWYcbSp8OH2o0cjbDTz3ACS1qCUA== dependencies: - "@lexical/utils" "0.11.2" + "@lexical/utils" "0.11.3" -"@lexical/markdown@0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@lexical/markdown/-/markdown-0.11.2.tgz#cb550e69cdbbac548b7e4f42a0e7a32e5a44b16b" - integrity sha512-4UyPSLuOFT1ezub5DPnwhbLPdaqWSTXSe7UBNw8Qt0/J3koAX4Kg099n+YcXJUwe04yJ00JKScsUjpDQXOOudw== +"@lexical/markdown@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@lexical/markdown/-/markdown-0.11.3.tgz#0656a33bdf8b506899c010bb44960f86276e346b" + integrity sha512-sF8ow32BDme3UvxaKpf+j+vMc4T/XvDEzteZHmvvP7NX/iUtK3yUkTyT7rKuGwiKLYfMBwQaKMGjU3/nlIOzUg== dependencies: - "@lexical/code" "0.11.2" - "@lexical/link" "0.11.2" - "@lexical/list" "0.11.2" - "@lexical/rich-text" "0.11.2" - "@lexical/text" "0.11.2" - "@lexical/utils" "0.11.2" + "@lexical/code" "0.11.3" + "@lexical/link" "0.11.3" + "@lexical/list" "0.11.3" + "@lexical/rich-text" "0.11.3" + "@lexical/text" "0.11.3" + "@lexical/utils" "0.11.3" -"@lexical/offset@0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@lexical/offset/-/offset-0.11.2.tgz#fd2b409c528fef1eb6f626465f606ccd65f0f134" - integrity sha512-9eYK8DV0DM24/mrMdJ9/6lOySIF10QUrePBDq9pzpk9i+S0cfyjnbLszUO+2Fk4/yiDUmMomSYa44K/kndqI3A== +"@lexical/offset@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@lexical/offset/-/offset-0.11.3.tgz#55c2ef5f036235d70f2aa762ed295e32dde9593a" + integrity sha512-3H9X8iqDSk0LrMOHZuqYuqX4EYGb78TIhtjrFbLJi/OgKmHaSeLx59xcMZdgd5kBdRitzQYMmvbRDvbLfMgWrA== -"@lexical/overflow@0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@lexical/overflow/-/overflow-0.11.2.tgz#f96674969f67fc556e82b9eebf237b80c4e39ef9" - integrity sha512-59c8Kk+ee2jU1RlnosIg323uWrRCIB9aYy7FMsKdH0Hhl+ZhkKI4BVA8rT0SINoq3kA//XwiCWclOQzm2Jmshw== +"@lexical/overflow@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@lexical/overflow/-/overflow-0.11.3.tgz#a77d72a4fdf8dcc4f558e68a5d0ac210f020472b" + integrity sha512-ShjCG8lICShOBKwrpP+9PjRFKEBCSUUMjbIGZfLnoL//3hyRtGv5aRgRyfJlRgDhCve0ROt5znLJV88EXzGRyA== -"@lexical/plain-text@0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@lexical/plain-text/-/plain-text-0.11.2.tgz#df69ccd1899411ff3ef51742399b3f10a05a77ab" - integrity sha512-WVipqB8ltK5tXr/5ZYeJp5ry/SVAXEffVgWqjj30D35nbekNDATkg/qzihm3wUVf1dB87U+nI2oiFJWLoIASuQ== +"@lexical/plain-text@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@lexical/plain-text/-/plain-text-0.11.3.tgz#ee56c0f0bc10a6333dc9790fb6f27b8d41887da8" + integrity sha512-cQ5Us+GNzShyjjgRqWTnYv0rC+jHJ96LvBA1aSieM77H8/Im5BeoLl6TgBK2NqPkp8fGpj8JnDEdT8h9Qh1jtA== -"@lexical/react@^0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@lexical/react/-/react-0.11.2.tgz#b86683f31f4ee365afafeceb4172819e35299abd" - integrity sha512-lRKdt+uc+MFQ5mQFUr/QpW9GtUdTAyDN0wlG/pLDYFIjU++kTrPhX/fK6SPevSRFm4lsLV2qKd8Ubj7ZOkmmJw== +"@lexical/react@^0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@lexical/react/-/react-0.11.3.tgz#e889170cf29bf71e3c4799a104d4f4c99499fa2e" + integrity sha512-Rn0Agnrz3uLIWbNyS9PRlkxOxcIDl2kxaVfgBacqQtYKR0ZVB2Hnoi89Cq6VmWPovauPyryx4Q3FC8Y11X7Otg== dependencies: - "@lexical/clipboard" "0.11.2" - "@lexical/code" "0.11.2" - "@lexical/dragon" "0.11.2" - "@lexical/hashtag" "0.11.2" - "@lexical/history" "0.11.2" - "@lexical/link" "0.11.2" - "@lexical/list" "0.11.2" - "@lexical/mark" "0.11.2" - "@lexical/markdown" "0.11.2" - "@lexical/overflow" "0.11.2" - "@lexical/plain-text" "0.11.2" - "@lexical/rich-text" "0.11.2" - "@lexical/selection" "0.11.2" - "@lexical/table" "0.11.2" - "@lexical/text" "0.11.2" - "@lexical/utils" "0.11.2" - "@lexical/yjs" "0.11.2" + "@lexical/clipboard" "0.11.3" + "@lexical/code" "0.11.3" + "@lexical/dragon" "0.11.3" + "@lexical/hashtag" "0.11.3" + "@lexical/history" "0.11.3" + "@lexical/link" "0.11.3" + "@lexical/list" "0.11.3" + "@lexical/mark" "0.11.3" + "@lexical/markdown" "0.11.3" + "@lexical/overflow" "0.11.3" + "@lexical/plain-text" "0.11.3" + "@lexical/rich-text" "0.11.3" + "@lexical/selection" "0.11.3" + "@lexical/table" "0.11.3" + "@lexical/text" "0.11.3" + "@lexical/utils" "0.11.3" + "@lexical/yjs" "0.11.3" react-error-boundary "^3.1.4" -"@lexical/rich-text@0.11.2", "@lexical/rich-text@^0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@lexical/rich-text/-/rich-text-0.11.2.tgz#12e1cf8bf92191de30f78905240ec3e437f2698d" - integrity sha512-/CvQPfmeFwKXSF3nYL5DBT3YLI8RKoIw2Stq0xHjvgo0iFtu0jgfkfbm3b1qxEeENpxkBKkOpq6b9j4GT2avew== +"@lexical/rich-text@0.11.3", "@lexical/rich-text@^0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@lexical/rich-text/-/rich-text-0.11.3.tgz#9501789bfe9671c220da7e95bf8589fa92cecf8c" + integrity sha512-fBFs6wMS7GFLbk+mzIWtwpP+EmnTZZ5bHpveuQ5wXONBuUuLcsYF5KO7UhLxXNLmiViV6lxatZPavEzgZdW7oQ== -"@lexical/selection@0.11.2", "@lexical/selection@^0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@lexical/selection/-/selection-0.11.2.tgz#8c9f6f1641611e561e3e7c7193678b390bf3ac24" - integrity sha512-hRkanEZsTxEH4J9MBd6htvV10lXfSZLj2MrQrA8XIduHq5uVNwvCFQBALA+7pvnp12AamAuf/HeROzn93laiLw== +"@lexical/selection@0.11.3", "@lexical/selection@^0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@lexical/selection/-/selection-0.11.3.tgz#f7250fae305a84c6e264a413f5feab056aeabfa3" + integrity sha512-15lQpcKT/vd7XZ5pnF1nb+kpKb72e9Yi1dVqieSxTeXkzt1cAZFKP3NB4RlhOKCv1N+glSBnjSxRwgsFfbD+NQ== -"@lexical/table@0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@lexical/table/-/table-0.11.2.tgz#a538611700ff04a26a9068e825e61e44887476ee" - integrity sha512-MxNKgO1XjbpkVakFgJ8dzkgIsvOJir9/jyJQ/dcePK+6RYnjzsSBHsKoZtdoZCVIWhmaV2HwDdHOdnbKpJ60Kw== +"@lexical/table@0.11.3", "@lexical/table@^0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@lexical/table/-/table-0.11.3.tgz#9b1980d828d7a588aaffa4cb8c6bb61401729034" + integrity sha512-EyRnN39CSPsMceADBR7Kf+sBHNpNQlPEkn/52epeDSnakR6s80woyrA3kIzKo6mLB4afvoqdYc7RfR96M9JLIA== dependencies: - "@lexical/utils" "0.11.2" + "@lexical/utils" "0.11.3" -"@lexical/text@0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@lexical/text/-/text-0.11.2.tgz#1ec1fef938568b7b294d8153b3c5830be9502411" - integrity sha512-XZmALZpzV+k9xmt2bhclKRTGhPlyZIvrDsxVADb+MEx6GpJk+VqYJqLgVpha5RiWtWKEjFXfU4pnPz56L3v/jA== +"@lexical/text@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@lexical/text/-/text-0.11.3.tgz#81ce2b5cd0caa9d89372e52c3548173d9395ad3d" + integrity sha512-gCEN8lJyR6b+yaOwKWGj79pbOfCQPWU/PHWyoNFUkEJXn3KydCzr2EYb6ta2cvQWRQU4G2BClKCR56jL4NS+qg== -"@lexical/utils@0.11.2", "@lexical/utils@^0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@lexical/utils/-/utils-0.11.2.tgz#c8558b99e044a2428eb5aeb6f6ff0fc24ab244ee" - integrity sha512-4wzEznp+Q4inCfF8XJmr+KWGGqFOk6o8dKH7DmyOBO7YjgzOsbX2Dz/ImVyjt0c3ROkLHisouBFb8GP5iWa0UA== +"@lexical/utils@0.11.3", "@lexical/utils@^0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@lexical/utils/-/utils-0.11.3.tgz#c4eb953289d29943974008c75f80c8d4e294e009" + integrity sha512-vC4saCrlcmyIJnvrYKw1uYxZojlD1DCIBsFlgmO8kXyRYXjj+o/8PBdn2dsgSQ3rADrC2mUloOm/maekDcYe9Q== dependencies: - "@lexical/list" "0.11.2" - "@lexical/selection" "0.11.2" - "@lexical/table" "0.11.2" + "@lexical/list" "0.11.3" + "@lexical/selection" "0.11.3" + "@lexical/table" "0.11.3" -"@lexical/yjs@0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@lexical/yjs/-/yjs-0.11.2.tgz#69a8202d19ee312320ef2fc21e124ac3d8d43af6" - integrity sha512-4lDAXP4jscNuG72Q8Jgb67YVJKrMO50lEIsKs3aEHKqpV1L3/2sgxekNy4s5uI0fIpMXxbsec4uxG8MuJO3UgQ== +"@lexical/yjs@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@lexical/yjs/-/yjs-0.11.3.tgz#b7049c0be85945fe0766b614256df8121bb05499" + integrity sha512-TLDQG2FSEw/aOfppEBb0wRlIuzJ57W//8ImfzyZvckSC12tvU0YKQQX8nQz/rybXdyfRy5eN+8gX5K2EyZx+pQ== dependencies: - "@lexical/offset" "0.11.2" + "@lexical/offset" "0.11.3" "@mdn/browser-compat-data@^3.3.14": version "3.3.14" @@ -9358,10 +9358,10 @@ lexical-remark@^0.3.8: unist-util-visit "^4.1.2" zwitch "^2.0.4" -lexical@^0.11.2: - version "0.11.2" - resolved "https://registry.yarnpkg.com/lexical/-/lexical-0.11.2.tgz#fe930c1cbeea1bec497a7d53c3e5543b6adb67d5" - integrity sha512-9pvMhn28Liac6uRVwhedIy7iw9IYTR6J1A2Ic+sfgdc5GbLBVZm1obCxazdbJzprRoQdrRUh7M4JJfQdCxC0MQ== +lexical@^0.11.3: + version "0.11.3" + resolved "https://registry.yarnpkg.com/lexical/-/lexical-0.11.3.tgz#1ad1a56a657eb55d1b9644733f271bf75a65cbe9" + integrity sha512-xsMKgx/Fa+QHg/nweemU04lCy7TnEr8LyeDtsKUC7fIDN9wH3GqbnQ0+e3Hbg4FmxlhDCiPPt0GcZAROq3R8uw== li@^1.3.0: version "1.3.0" @@ -11995,7 +11995,7 @@ regexp.prototype.flags@^1.3.1: call-bind "^1.0.2" define-properties "^1.1.3" -regexp.prototype.flags@^1.5.0: +regexp.prototype.flags@^1.4.3, regexp.prototype.flags@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz#fe7ce25e7e4cca8db37b6634c8a2c7009199b9cb" integrity sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==