diff --git a/packages/web/src/javascripts/Components/SuperEditor/Lexical/Nodes/TableComponent.tsx b/packages/web/src/javascripts/Components/SuperEditor/Lexical/Nodes/TableComponent.tsx
deleted file mode 100644
index 1c2901a6c42..00000000000
--- a/packages/web/src/javascripts/Components/SuperEditor/Lexical/Nodes/TableComponent.tsx
+++ /dev/null
@@ -1,1546 +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.
- *
- */
-
-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 {
- RangeSelection,
- TextFormatType,
- $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 { createPortal } from 'react-dom'
-import { IS_APPLE } from '../Shared/environment'
-import { CellContext } from '../../Plugins/TablePlugin'
-import {
- $isTableNode,
- Cell,
- cellHTMLCache,
- cellTextContentCache,
- createRow,
- createUID,
- exportTableCellsToHTML,
- extractRowsFromHTML,
- Rows,
- TableNode,
-} from './TableNode'
-
-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 == null) {
- 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 IS_APPLE ? metaKey : ctrlKey
- }
-
- return false
-}
-
-function isCut(keyCode: number, shiftKey: boolean, metaKey: boolean, ctrlKey: boolean): boolean {
- if (shiftKey) {
- return false
- }
- if (keyCode === 88) {
- return IS_APPLE ? metaKey : ctrlKey
- }
-
- return false
-}
-
-function isPaste(keyCode: number, shiftKey: boolean, metaKey: boolean, ctrlKey: boolean): boolean {
- if (shiftKey) {
- return false
- }
- if (keyCode === 86) {
- return IS_APPLE ? 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 != null) {
- 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 (
- {
- 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)
- 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 != null) {
- clipboardData.setData('text/html', htmlString)
- clipboardData.setData('text/plain', plainTextString)
- clipboardData.setData('application/x-lexical-editor', lexicalString)
- } else {
- const clipboard = navigator.clipboard
- if (clipboard != null) {
- // 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).catch(console.error)
- }
- }
- }
-
- 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 != null) {
- 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 != null) {
- $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).catch(console.error)
- 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,
- () => {
- 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 &&
}
- {showAddRows &&
}
- {resizingID !== null &&
}
-
- )
-}
diff --git a/packages/web/src/javascripts/Components/SuperEditor/Lexical/Nodes/TableNode.tsx b/packages/web/src/javascripts/Components/SuperEditor/Lexical/Nodes/TableNode.tsx
deleted file mode 100644
index e788333999d..00000000000
--- a/packages/web/src/javascripts/Components/SuperEditor/Lexical/Nodes/TableNode.tsx
+++ /dev/null
@@ -1,390 +0,0 @@
-import {
- DOMConversionMap,
- DOMConversionOutput,
- DOMExportOutput,
- EditorConfig,
- LexicalEditor,
- LexicalNode,
- NodeKey,
- SerializedLexicalNode,
- Spread,
- DecoratorNode,
-} from 'lexical'
-
-import * as React from 'react'
-import { Suspense } from 'react'
-
-export type Cell = {
- colSpan: number
- json: string
- type: 'normal' | 'header'
- id: string
- width: number | null
-}
-
-export type Row = {
- cells: Array
- height: null | number
- id: string
-}
-
-export type Rows = Array
-
-export const cellHTMLCache: Map = new Map()
-export const cellTextContentCache: Map = new Map()
-
-const emptyEditorJSON =
- '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'
-
-const plainTextEditorJSON = (text: string) => {
- return text === ''
- ? emptyEditorJSON
- : `{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":${text},"type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}`
-}
-
-const TableComponent = React.lazy(
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- () => import('./TableComponent'),
-)
-
-export function createUID(): string {
- return Math.random()
- .toString(36)
- .replace(/[^a-z]+/g, '')
- .substr(0, 5)
-}
-
-function createCell(type: 'normal' | 'header'): Cell {
- return {
- colSpan: 1,
- id: createUID(),
- json: emptyEditorJSON,
- type,
- width: null,
- }
-}
-
-export function createRow(): Row {
- return {
- cells: [],
- height: null,
- id: createUID(),
- }
-}
-
-export type SerializedTableNode = Spread<
- {
- rows: Rows
- type: 'tablesheet'
- version: 1
- },
- SerializedLexicalNode
->
-
-export function extractRowsFromHTML(tableElem: HTMLTableElement): Rows {
- const rowElems = tableElem.querySelectorAll('tr')
- const rows: Rows = []
- for (let y = 0; y < rowElems.length; y++) {
- const rowElem = rowElems[y]
- const cellElems = rowElem.querySelectorAll('td,th')
- if (!cellElems || cellElems.length === 0) {
- continue
- }
- const cells: Array = []
- for (let x = 0; x < cellElems.length; x++) {
- const cellElem = cellElems[x] as HTMLElement
- const isHeader = cellElem.nodeName === 'TH'
- const cell = createCell(isHeader ? 'header' : 'normal')
- cell.json = plainTextEditorJSON(JSON.stringify(cellElem.innerText.replace(/\n/g, ' ')))
- cells.push(cell)
- }
- const row = createRow()
- row.cells = cells
- rows.push(row)
- }
- return rows
-}
-
-function convertTableElement(domNode: HTMLElement): null | DOMConversionOutput {
- const rowElems = domNode.querySelectorAll('tr')
- if (!rowElems || rowElems.length === 0) {
- return null
- }
- const rows: Rows = []
- for (let y = 0; y < rowElems.length; y++) {
- const rowElem = rowElems[y]
- const cellElems = rowElem.querySelectorAll('td,th')
- if (!cellElems || cellElems.length === 0) {
- continue
- }
- const cells: Array = []
- for (let x = 0; x < cellElems.length; x++) {
- const cellElem = cellElems[x] as HTMLElement
- const isHeader = cellElem.nodeName === 'TH'
- const cell = createCell(isHeader ? 'header' : 'normal')
- cell.json = plainTextEditorJSON(JSON.stringify(cellElem.innerText.replace(/\n/g, ' ')))
- cells.push(cell)
- }
- const row = createRow()
- row.cells = cells
- rows.push(row)
- }
- return { node: $createTableNode(rows) }
-}
-
-export function exportTableCellsToHTML(
- rows: Rows,
- rect?: { startX: number; endX: number; startY: number; endY: number },
-): HTMLElement {
- const table = document.createElement('table')
- const colGroup = document.createElement('colgroup')
- const tBody = document.createElement('tbody')
- const firstRow = rows[0]
-
- for (let x = rect != null ? rect.startX : 0; x < (rect != null ? rect.endX + 1 : firstRow.cells.length); x++) {
- const col = document.createElement('col')
- colGroup.append(col)
- }
-
- for (let y = rect != null ? rect.startY : 0; y < (rect != null ? rect.endY + 1 : rows.length); y++) {
- const row = rows[y]
- const cells = row.cells
- const rowElem = document.createElement('tr')
-
- for (let x = rect != null ? rect.startX : 0; x < (rect != null ? rect.endX + 1 : cells.length); x++) {
- const cell = cells[x]
- const cellElem = document.createElement(cell.type === 'header' ? 'th' : 'td')
- cellElem.innerHTML = cellHTMLCache.get(cell.json) || ''
- rowElem.appendChild(cellElem)
- }
- tBody.appendChild(rowElem)
- }
-
- table.appendChild(colGroup)
- table.appendChild(tBody)
- return table
-}
-
-export class TableNode extends DecoratorNode {
- __rows: Rows
-
- static override getType(): string {
- return 'tablesheet'
- }
-
- static override clone(node: TableNode): TableNode {
- return new TableNode(Array.from(node.__rows), node.__key)
- }
-
- static override importJSON(serializedNode: SerializedTableNode): TableNode {
- return $createTableNode(serializedNode.rows)
- }
-
- override exportJSON(): SerializedTableNode {
- return {
- rows: this.__rows,
- type: 'tablesheet',
- version: 1,
- }
- }
-
- static importDOM(): DOMConversionMap | null {
- return {
- table: (_node: Node) => ({
- conversion: convertTableElement,
- priority: 0,
- }),
- }
- }
-
- override exportDOM(): DOMExportOutput {
- return { element: exportTableCellsToHTML(this.__rows) }
- }
-
- constructor(rows?: Rows, key?: NodeKey) {
- super(key)
- this.__rows = rows || []
- }
-
- override createDOM(): HTMLElement {
- const div = document.createElement('div')
- div.style.display = 'contents'
- return div
- }
-
- override updateDOM(): false {
- return false
- }
-
- mergeRows(startX: number, startY: number, mergeRows: Rows): void {
- const self = this.getWritable()
- const rows = self.__rows
- const endY = Math.min(rows.length, startY + mergeRows.length)
- for (let y = startY; y < endY; y++) {
- const row = rows[y]
- const mergeRow = mergeRows[y - startY]
- const cells = row.cells
- const cellsClone = Array.from(cells)
- const rowClone = { ...row, cells: cellsClone }
- const mergeCells = mergeRow.cells
- const endX = Math.min(cells.length, startX + mergeCells.length)
- for (let x = startX; x < endX; x++) {
- const cell = cells[x]
- const mergeCell = mergeCells[x - startX]
- const cellClone = { ...cell, json: mergeCell.json, type: mergeCell.type }
- cellsClone[x] = cellClone
- }
- rows[y] = rowClone
- }
- }
-
- updateCellJSON(x: number, y: number, json: string): void {
- const self = this.getWritable()
- const rows = self.__rows
- const row = rows[y]
- const cells = row.cells
- const cell = cells[x]
- const cellsClone = Array.from(cells)
- const cellClone = { ...cell, json }
- const rowClone = { ...row, cells: cellsClone }
- cellsClone[x] = cellClone
- rows[y] = rowClone
- }
-
- updateCellType(x: number, y: number, type: 'header' | 'normal'): void {
- const self = this.getWritable()
- const rows = self.__rows
- const row = rows[y]
- const cells = row.cells
- const cell = cells[x]
- const cellsClone = Array.from(cells)
- const cellClone = { ...cell, type }
- const rowClone = { ...row, cells: cellsClone }
- cellsClone[x] = cellClone
- rows[y] = rowClone
- }
-
- insertColumnAt(x: number): void {
- const self = this.getWritable()
- const rows = self.__rows
- for (let y = 0; y < rows.length; y++) {
- const row = rows[y]
- const cells = row.cells
- const cellsClone = Array.from(cells)
- const rowClone = { ...row, cells: cellsClone }
- const type = (cells[x] || cells[x - 1]).type
- cellsClone.splice(x, 0, createCell(type))
- rows[y] = rowClone
- }
- }
-
- deleteColumnAt(x: number): void {
- const self = this.getWritable()
- const rows = self.__rows
- for (let y = 0; y < rows.length; y++) {
- const row = rows[y]
- const cells = row.cells
- const cellsClone = Array.from(cells)
- const rowClone = { ...row, cells: cellsClone }
- cellsClone.splice(x, 1)
- rows[y] = rowClone
- }
- }
-
- addColumns(count: number): void {
- const self = this.getWritable()
- const rows = self.__rows
- for (let y = 0; y < rows.length; y++) {
- const row = rows[y]
- const cells = row.cells
- const cellsClone = Array.from(cells)
- const rowClone = { ...row, cells: cellsClone }
- const type = cells[cells.length - 1].type
- for (let x = 0; x < count; x++) {
- cellsClone.push(createCell(type))
- }
- rows[y] = rowClone
- }
- }
-
- insertRowAt(y: number): void {
- const self = this.getWritable()
- const rows = self.__rows
- const prevRow = rows[y] || rows[y - 1]
- const cellCount = prevRow.cells.length
- const row = createRow()
- for (let x = 0; x < cellCount; x++) {
- const cell = createCell(prevRow.cells[x].type)
- row.cells.push(cell)
- }
- rows.splice(y, 0, row)
- }
-
- deleteRowAt(y: number): void {
- const self = this.getWritable()
- const rows = self.__rows
- rows.splice(y, 1)
- }
-
- addRows(count: number): void {
- const self = this.getWritable()
- const rows = self.__rows
- const prevRow = rows[rows.length - 1]
- const cellCount = prevRow.cells.length
-
- for (let y = 0; y < count; y++) {
- const row = createRow()
- for (let x = 0; x < cellCount; x++) {
- const cell = createCell(prevRow.cells[x].type)
- row.cells.push(cell)
- }
- rows.push(row)
- }
- }
-
- updateColumnWidth(x: number, width: number): void {
- const self = this.getWritable()
- const rows = self.__rows
- for (let y = 0; y < rows.length; y++) {
- const row = rows[y]
- const cells = row.cells
- const cellsClone = Array.from(cells)
- const rowClone = { ...row, cells: cellsClone }
- cellsClone[x].width = width
- rows[y] = rowClone
- }
- }
-
- override decorate(_: LexicalEditor, config: EditorConfig): JSX.Element {
- return (
-
-
-
- )
- }
-}
-
-export function $isTableNode(node: LexicalNode | null | undefined): node is TableNode {
- return node instanceof TableNode
-}
-
-export function $createTableNode(rows: Rows): TableNode {
- return new TableNode(rows)
-}
-
-export function $createTableNodeWithDimensions(
- rowCount: number,
- columnCount: number,
- includeHeaders = true,
-): TableNode {
- const rows: Rows = []
- for (let y = 0; y < columnCount; y++) {
- const row: Row = createRow()
- rows.push(row)
- for (let x = 0; x < rowCount; x++) {
- row.cells.push(createCell(includeHeaders === true && (y === 0 || x === 0) ? 'header' : 'normal'))
- }
- }
- return new TableNode(rows)
-}
diff --git a/packages/web/src/javascripts/Components/SuperEditor/Lexical/Nodes/YouTubeNode.tsx b/packages/web/src/javascripts/Components/SuperEditor/Lexical/Nodes/YouTubeNode.tsx
index c1f9b6e2431..8984c247226 100644
--- a/packages/web/src/javascripts/Components/SuperEditor/Lexical/Nodes/YouTubeNode.tsx
+++ b/packages/web/src/javascripts/Components/SuperEditor/Lexical/Nodes/YouTubeNode.tsx
@@ -6,7 +6,17 @@
*
*/
-import type { EditorConfig, ElementFormatType, LexicalEditor, LexicalNode, NodeKey, Spread } from 'lexical'
+import type {
+ DOMConversionMap,
+ DOMConversionOutput,
+ DOMExportOutput,
+ EditorConfig,
+ ElementFormatType,
+ LexicalEditor,
+ LexicalNode,
+ NodeKey,
+ Spread,
+} from 'lexical'
import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents'
import { DecoratorBlockNode, SerializedDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
@@ -46,6 +56,15 @@ export type SerializedYouTubeNode = Spread<
SerializedDecoratorBlockNode
>
+function convertYoutubeElement(domNode: HTMLElement): null | DOMConversionOutput {
+ const videoID = domNode.getAttribute('data-lexical-youtube')
+ if (videoID) {
+ const node = $createYouTubeNode(videoID)
+ return { node }
+ }
+ return null
+}
+
export class YouTubeNode extends DecoratorBlockNode {
__id: string
@@ -72,6 +91,36 @@ export class YouTubeNode extends DecoratorBlockNode {
}
}
+ exportDOM(): DOMExportOutput {
+ const element = document.createElement('iframe')
+ element.setAttribute('data-lexical-youtube', this.__id)
+ element.setAttribute('width', '560')
+ element.setAttribute('height', '315')
+ element.setAttribute('src', `https://www.youtube.com/embed/${this.__id}`)
+ element.setAttribute('frameborder', '0')
+ element.setAttribute(
+ 'allow',
+ 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
+ )
+ element.setAttribute('allowfullscreen', 'true')
+ element.setAttribute('title', 'YouTube video')
+ return { element }
+ }
+
+ static importDOM(): DOMConversionMap | null {
+ return {
+ iframe: (domNode: HTMLElement) => {
+ if (!domNode.hasAttribute('data-lexical-youtube')) {
+ return null
+ }
+ return {
+ conversion: convertYoutubeElement,
+ priority: 1,
+ }
+ },
+ }
+ }
+
constructor(id: string, format?: ElementFormatType, key?: NodeKey) {
super(format, key)
this.__id = id
diff --git a/packages/web/src/javascripts/Components/SuperEditor/Lexical/Shared/environment.ts b/packages/web/src/javascripts/Components/SuperEditor/Lexical/Shared/environment.ts
index 785ddc4f64e..67784db97dd 100644
--- a/packages/web/src/javascripts/Components/SuperEditor/Lexical/Shared/environment.ts
+++ b/packages/web/src/javascripts/Components/SuperEditor/Lexical/Shared/environment.ts
@@ -33,5 +33,7 @@ export const IS_IOS: boolean = CAN_USE_DOM && /iPad|iPhone|iPod/.test(navigator.
// Keep these in case we need to use them in the future.
// export const IS_WINDOWS: boolean = CAN_USE_DOM && /Win/.test(navigator.platform);
-// export const IS_CHROME: boolean = CAN_USE_DOM && /^(?=.*Chrome).*/i.test(navigator.userAgent);
+export const IS_CHROME: boolean = CAN_USE_DOM && /^(?=.*Chrome).*/i.test(navigator.userAgent)
// export const canUseTextInputEvent: boolean = CAN_USE_DOM && 'TextEvent' in window && !documentMode;
+
+export const IS_APPLE_WEBKIT = CAN_USE_DOM && /AppleWebKit\/[\d.]+/.test(navigator.userAgent) && !IS_CHROME
diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/AutoEmbedPlugin/index.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/AutoEmbedPlugin/index.tsx
index c5245c87681..7a5ff405265 100644
--- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/AutoEmbedPlugin/index.tsx
+++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/AutoEmbedPlugin/index.tsx
@@ -16,7 +16,7 @@ import {
URL_MATCHER,
} from '@lexical/react/LexicalAutoEmbedPlugin'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
-import { useState } from 'react'
+import { useMemo, useState } from 'react'
import * as ReactDOM from 'react-dom'
import useModal from '../../Lexical/Hooks/useModal'
@@ -174,6 +174,18 @@ function AutoEmbedMenu({
)
}
+const debounce = (callback: (text: string) => void, delay: number) => {
+ let timeoutId: number
+
+ return (text: string) => {
+ window.clearTimeout(timeoutId)
+
+ timeoutId = window.setTimeout(() => {
+ callback(text)
+ }, delay)
+ }
+}
+
export function AutoEmbedDialog({
embedConfig,
onClose,
@@ -183,14 +195,26 @@ export function AutoEmbedDialog({
}): JSX.Element {
const [text, setText] = useState('')
const [editor] = useLexicalComposerContext()
+ const [embedResult, setEmbedResult] = useState(null)
+
+ const validateText = useMemo(
+ () =>
+ debounce((inputText: string) => {
+ const urlMatch = URL_MATCHER.exec(inputText)
+ if (embedConfig != null && inputText != null && urlMatch != null) {
+ void Promise.resolve(embedConfig.parseUrl(inputText)).then((parseResult) => {
+ setEmbedResult(parseResult)
+ })
+ } else if (embedResult != null) {
+ setEmbedResult(null)
+ }
+ }, 200),
+ [embedConfig, embedResult],
+ )
- const urlMatch = URL_MATCHER.exec(text)
- const embedResult = text != null && urlMatch != null ? embedConfig.parseUrl(text) : null
-
- const onClick = async () => {
- const result = await embedResult
- if (result != null) {
- embedConfig.insertNode(editor, result)
+ const onClick = () => {
+ if (embedResult != null) {
+ embedConfig.insertNode(editor, embedResult)
onClose()
}
}
@@ -205,7 +229,9 @@ export function AutoEmbedDialog({
value={text}
data-test-id={`${embedConfig.type}-embed-modal-url`}
onChange={(e) => {
- setText(e.target.value)
+ const { value } = e.target
+ setText(value)
+ validateText(value)
}}
/>
diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/AutoLinkPlugin/AutoLinkPlugin.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/AutoLinkPlugin/AutoLinkPlugin.tsx
index 431d01fe4d4..23bf6583ab4 100644
--- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/AutoLinkPlugin/AutoLinkPlugin.tsx
+++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/AutoLinkPlugin/AutoLinkPlugin.tsx
@@ -1,55 +1,28 @@
-import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
-import { AutoLinkPlugin as LexicalAutoLinkPlugin } from '@lexical/react/LexicalAutoLinkPlugin'
-import { COMMAND_PRIORITY_EDITOR, KEY_MODIFIER_COMMAND, $getSelection } from 'lexical'
-import { useEffect } from 'react'
-import { TOGGLE_LINK_COMMAND } from '@lexical/link'
-import { mergeRegister } from '@lexical/utils'
+/**
+ * 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.
+ *
+ */
-const URL_MATCHER =
+import { AutoLinkPlugin, createLinkMatcherWithRegExp } from '@lexical/react/LexicalAutoLinkPlugin'
+
+const URL_REGEX =
/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/
+const EMAIL_REGEX =
+ /(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/
+
const MATCHERS = [
- (text: string) => {
- const match = URL_MATCHER.exec(text)
- if (match === null) {
- return null
- }
- const fullMatch = match[0]
- return {
- index: match.index,
- length: fullMatch.length,
- text: fullMatch,
- url: fullMatch.startsWith('http') ? fullMatch : `https://${fullMatch}`,
- }
- },
+ createLinkMatcherWithRegExp(URL_REGEX, (text) => {
+ return text.startsWith('http') ? text : `https://${text}`
+ }),
+ createLinkMatcherWithRegExp(EMAIL_REGEX, (text) => {
+ return `mailto:${text}`
+ }),
]
-export default function AutoLinkPlugin(): JSX.Element | null {
- const [editor] = useLexicalComposerContext()
-
- useEffect(() => {
- return mergeRegister(
- editor.registerCommand(
- KEY_MODIFIER_COMMAND,
- (event: KeyboardEvent) => {
- const isCmdK = event.key === 'k' && !event.altKey && (event.metaKey || event.ctrlKey)
- if (isCmdK) {
- const selection = $getSelection()
- if (selection) {
- editor.dispatchCommand(TOGGLE_LINK_COMMAND, selection.getTextContent())
- }
- }
-
- return false
- },
- COMMAND_PRIORITY_EDITOR,
- ),
- )
- }, [editor])
-
- return (
- <>
-
- >
- )
+export default function LexicalAutoLinkPlugin(): JSX.Element {
+ return
}
diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/FloatingLinkEditorPlugin/index.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/FloatingLinkEditorPlugin/index.tsx
index ffbb2db494e..25d95726a6b 100644
--- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/FloatingLinkEditorPlugin/index.tsx
+++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/FloatingLinkEditorPlugin/index.tsx
@@ -200,7 +200,7 @@ function useFloatingLinkEditorToolbar(editor: LexicalEditor, anchorElem: HTMLEle
const linkParent = $findMatchingParent(node, $isLinkNode)
const autoLinkParent = $findMatchingParent(node, $isAutoLinkNode)
- if (linkParent != null || autoLinkParent != null) {
+ if (linkParent != null && autoLinkParent == null) {
setIsLink(true)
} else {
setIsLink(false)
diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/TablePlugin.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/TablePlugin.tsx
index c22dde2cef9..6733b3e78d5 100644
--- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/TablePlugin.tsx
+++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/TablePlugin.tsx
@@ -6,90 +6,13 @@
*
*/
-import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { INSERT_TABLE_COMMAND } from '@lexical/table'
-import {
- $createNodeSelection,
- $createParagraphNode,
- $getSelection,
- $isRangeSelection,
- $isRootOrShadowRoot,
- $setSelection,
- 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 invariant from '../Lexical/Shared/invariant'
-import { $createTableNodeWithDimensions, TableNode } from '../Lexical/Nodes/TableNode'
+import { LexicalEditor } from 'lexical'
+import { useState } from 'react'
import Button from '../Lexical/UI/Button'
import { DialogActions } from '../Lexical/UI/Dialog'
import TextInput from '../Lexical/UI/TextInput'
-export type InsertTableCommandPayload = Readonly<{
- columns: string
- rows: string
- 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')
-
-// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-// @ts-ignore: not sure why TS doesn't like using null as the value?
-export const CellContext: React.Context = 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 InsertTableDialog({
activeEditor,
onClose,
@@ -115,91 +38,3 @@ export function InsertTableDialog({
>
)
}
-
-export function InsertNewTableDialog({
- activeEditor,
- onClose,
-}: {
- activeEditor: LexicalEditor
- onClose: () => void
-}): JSX.Element {
- const [rows, setRows] = useState('5')
- const [columns, setColumns] = useState('5')
-
- const onClick = () => {
- activeEditor.dispatchCommand(INSERT_NEW_TABLE_COMMAND, { columns, rows })
- onClose()
- }
-
- return (
- <>
-
-
-
-
-
- >
- )
-}
-
-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])) {
- invariant(false, 'TablePlugin: TableNode is not registered on editor')
- }
-
- cellContext.set(cellEditorConfig, children)
-
- return editor.registerCommand(
- INSERT_TABLE_COMMAND,
- ({ columns, rows, includeHeaders }) => {
- const selection = $getSelection()
-
- if (!$isRangeSelection(selection)) {
- return true
- }
-
- const focus = selection.focus
- const focusNode = focus.getNode()
-
- if (focusNode !== null) {
- const tableNode = $createTableNodeWithDimensions(Number(rows), Number(columns), includeHeaders)
-
- if ($isRootOrShadowRoot(focusNode)) {
- const target = focusNode.getChildAtIndex(focus.offset)
-
- if (target !== null) {
- target.insertBefore(tableNode)
- } else {
- focusNode.append(tableNode)
- }
-
- tableNode.insertBefore($createParagraphNode())
- } else {
- const topLevelNode = focusNode.getTopLevelElementOrThrow()
- topLevelNode.insertAfter(tableNode)
- }
-
- tableNode.insertAfter($createParagraphNode())
- const nodeSelection = $createNodeSelection()
- nodeSelection.add(tableNode.getKey())
- $setSelection(nodeSelection)
- }
-
- return true
- },
- COMMAND_PRIORITY_EDITOR,
- )
- }, [cellContext, cellEditorConfig, children, editor])
-
- return null
-}
| | |