From 0b91a2052a7ddedd56daca9a48e580d6d6af20e8 Mon Sep 17 00:00:00 2001 From: Mathias Jakobsen Date: Mon, 14 Aug 2023 09:16:14 +0100 Subject: [PATCH] Merge pull request #14233 from overleaf/mj-table-generator-skip-hlines [visual] Table generator tweaks GitOrigin-RevId: 80ec32d024d185861a3635d5cc6d77d6a7031b64 --- .../components/table-generator/cell.tsx | 55 ++++++++++++++--- .../contexts/editing-context.tsx | 19 ++++++ .../contexts/selection-context.tsx | 7 ++- .../contexts/tabular-context.tsx | 22 +++++++ .../components/table-generator/table.tsx | 51 +++++++++++++++- .../components/table-generator/tabular.tsx | 61 +++++++++++++++---- .../toolbar/toolbar-button-menu.tsx | 8 +-- .../toolbar/toolbar-dropdown.tsx | 8 +-- .../components/table-generator/utils.ts | 5 ++ .../extensions/toolbar/toolbar-panel.ts | 2 + 10 files changed, 204 insertions(+), 34 deletions(-) create mode 100644 services/web/frontend/js/features/source-editor/components/table-generator/contexts/tabular-context.tsx diff --git a/services/web/frontend/js/features/source-editor/components/table-generator/cell.tsx b/services/web/frontend/js/features/source-editor/components/table-generator/cell.tsx index 7991b7f057..3fbdefd0ed 100644 --- a/services/web/frontend/js/features/source-editor/components/table-generator/cell.tsx +++ b/services/web/frontend/js/features/source-editor/components/table-generator/cell.tsx @@ -17,7 +17,8 @@ export const Cell: FC<{ columnIndex: number row: RowData }> = ({ cellData, columnSpecification, rowIndex, columnIndex, row }) => { - const { selection, setSelection } = useSelectionContext() + const { selection, setSelection, dragging, setDragging } = + useSelectionContext() const renderDiv = useRef(null) const cellRef = useRef(null) const { @@ -36,22 +37,56 @@ export const Cell: FC<{ if (event.button !== 0) { return } - event.stopPropagation() + setDragging(true) + document.getSelection()?.empty() setSelection(current => { if (event.shiftKey && current) { return new TableSelection(current.from, { - row: rowIndex, cell: columnIndex, + row: rowIndex, }) - } else { - return new TableSelection( - { row: rowIndex, cell: columnIndex }, - { row: rowIndex, cell: columnIndex } - ) } + return new TableSelection({ cell: columnIndex, row: rowIndex }) }) }, - [setSelection, rowIndex, columnIndex] + [setDragging, columnIndex, rowIndex, setSelection] + ) + + const onMouseUp = useCallback(() => { + if (dragging) { + setDragging(false) + } + }, [setDragging, dragging]) + + const onMouseMove: MouseEventHandler = useCallback( + event => { + if (dragging) { + if (event.buttons !== 1) { + setDragging(false) + return + } + document.getSelection()?.empty() + if ( + selection?.to.cell === columnIndex && + selection?.to.row === rowIndex + ) { + // Do nothing if selection has remained the same + return + } + event.stopPropagation() + setSelection(current => { + if (current) { + return new TableSelection(current.from, { + row: rowIndex, + cell: columnIndex, + }) + } else { + return new TableSelection({ row: rowIndex, cell: columnIndex }) + } + }) + } + }, + [dragging, columnIndex, rowIndex, setSelection, selection, setDragging] ) useEffect(() => { @@ -122,6 +157,8 @@ export const Cell: FC<{ onDoubleClick={onDoubleClick} tabIndex={row.cells.length * rowIndex + columnIndex + 1} onMouseDown={onMouseDown} + onMouseUp={onMouseUp} + onMouseMove={onMouseMove} ref={cellRef} className={classNames('table-generator-cell', { 'table-generator-cell-border-left': columnSpecification.borderLeft > 0, diff --git a/services/web/frontend/js/features/source-editor/components/table-generator/contexts/editing-context.tsx b/services/web/frontend/js/features/source-editor/components/table-generator/contexts/editing-context.tsx index 3e982d57e6..2f464bc24a 100644 --- a/services/web/frontend/js/features/source-editor/components/table-generator/contexts/editing-context.tsx +++ b/services/web/frontend/js/features/source-editor/components/table-generator/contexts/editing-context.tsx @@ -1,6 +1,7 @@ import { FC, createContext, useCallback, useContext, useState } from 'react' import { useCodeMirrorViewContext } from '../../codemirror-editor' import { useTableContext } from './table-context' +import { TableSelection } from './selection-context' type EditingContextData = { rowIndex: number @@ -15,6 +16,7 @@ const EditingContext = createContext< updateCellData: (content: string) => void cancelEditing: () => void commitCellData: () => void + clearCells: (selection: TableSelection) => void startEditing: ( rowIndex: number, cellIndex: number, @@ -84,6 +86,22 @@ export const EditingContextProvider: FC = ({ children }) => { }, [setCellData] ) + + const clearCells = useCallback( + (selection: TableSelection) => { + const changes: { from: number; to: number; insert: '' }[] = [] + const { minX, minY, maxX, maxY } = selection.normalized() + for (let row = minY; row <= maxY; row++) { + for (let cell = minX; cell <= maxX; cell++) { + const { from, to } = cellPositions[row][cell] + changes.push({ from, to, insert: '' }) + } + } + view.dispatch({ changes }) + }, + [view, cellPositions] + ) + return ( { cancelEditing, commitCellData, startEditing, + clearCells, }} > {children} diff --git a/services/web/frontend/js/features/source-editor/components/table-generator/contexts/selection-context.tsx b/services/web/frontend/js/features/source-editor/components/table-generator/contexts/selection-context.tsx index 6f7b36d5d8..3c171067a7 100644 --- a/services/web/frontend/js/features/source-editor/components/table-generator/contexts/selection-context.tsx +++ b/services/web/frontend/js/features/source-editor/components/table-generator/contexts/selection-context.tsx @@ -152,6 +152,8 @@ const SelectionContext = createContext< | { selection: TableSelection | null setSelection: Dispatch> + dragging: boolean + setDragging: Dispatch> } | undefined >(undefined) @@ -170,8 +172,11 @@ export const useSelectionContext = () => { export const SelectionContextProvider: FC = ({ children }) => { const [selection, setSelection] = useState(null) + const [dragging, setDragging] = useState(false) return ( - + {children} ) diff --git a/services/web/frontend/js/features/source-editor/components/table-generator/contexts/tabular-context.tsx b/services/web/frontend/js/features/source-editor/components/table-generator/contexts/tabular-context.tsx new file mode 100644 index 0000000000..60baceab42 --- /dev/null +++ b/services/web/frontend/js/features/source-editor/components/table-generator/contexts/tabular-context.tsx @@ -0,0 +1,22 @@ +import { FC, RefObject, createContext, useContext, useRef } from 'react' + +const TabularContext = createContext< + { ref: RefObject } | undefined +>(undefined) + +export const TabularProvider: FC = ({ children }) => { + const ref = useRef(null) + return ( + + {children} + + ) +} + +export const useTabularContext = () => { + const tabularContext = useContext(TabularContext) + if (!tabularContext) { + throw new Error('TabularContext must be used within TabularProvider') + } + return tabularContext +} diff --git a/services/web/frontend/js/features/source-editor/components/table-generator/table.tsx b/services/web/frontend/js/features/source-editor/components/table-generator/table.tsx index 527575eaf7..8c6fbce890 100644 --- a/services/web/frontend/js/features/source-editor/components/table-generator/table.tsx +++ b/services/web/frontend/js/features/source-editor/components/table-generator/table.tsx @@ -1,4 +1,11 @@ -import { FC, KeyboardEventHandler, useCallback, useMemo, useRef } from 'react' +import { + FC, + KeyboardEvent, + KeyboardEventHandler, + useCallback, + useMemo, + useRef, +} from 'react' import { Row } from './row' import { ColumnSelector } from './selectors' import { @@ -23,8 +30,14 @@ type NavigationMap = { export const Table: FC = () => { const { selection, setSelection } = useSelectionContext() - const { cellData, cancelEditing, startEditing, commitCellData } = - useEditingContext() + const { + cellData, + cancelEditing, + startEditing, + commitCellData, + clearCells, + updateCellData, + } = useEditingContext() const { table: tableData } = useTableContext() const tableRef = useRef(null) const view = useCodeMirrorViewContext() @@ -50,6 +63,15 @@ export const Table: FC = () => { [selection, tableData.columns.length, tableData.rows.length] ) + const isCharacterInput = useCallback((event: KeyboardEvent) => { + return ( + event.key?.length === 1 && + !event.ctrlKey && + !event.metaKey && + !event.altKey + ) + }, []) + const onKeyDown: KeyboardEventHandler = useCallback( event => { if (event.code === 'Enter') { @@ -74,6 +96,16 @@ export const Table: FC = () => { } else { cancelEditing() } + } else if (event.code === 'Delete' || event.code === 'Backspace') { + if (cellData) { + return + } + if (!selection) { + return + } + event.preventDefault() + event.stopPropagation() + clearCells(selection) } else if (Object.prototype.hasOwnProperty.call(navigation, event.code)) { const [defaultNavigation, shiftNavigation] = navigation[event.code as NavigationKey] @@ -92,6 +124,16 @@ export const Table: FC = () => { } else { setSelection(defaultNavigation()) } + } else if (isCharacterInput(event) && !cellData) { + event.preventDefault() + event.stopPropagation() + if (!selection) { + return + } + const cell = tableData.rows[selection.to.row].cells[selection.to.cell] + startEditing(selection.to.row, selection.to.cell, cell.content) + updateCellData(event.key) + setSelection(new TableSelection(selection.to, selection.to)) } }, [ @@ -104,6 +146,9 @@ export const Table: FC = () => { commitCellData, navigation, view, + clearCells, + updateCellData, + isCharacterInput, ] ) return ( diff --git a/services/web/frontend/js/features/source-editor/components/table-generator/tabular.tsx b/services/web/frontend/js/features/source-editor/components/table-generator/tabular.tsx index eb04baf1cb..2f116a687e 100644 --- a/services/web/frontend/js/features/source-editor/components/table-generator/tabular.tsx +++ b/services/web/frontend/js/features/source-editor/components/table-generator/tabular.tsx @@ -1,16 +1,23 @@ import { SyntaxNode } from '@lezer/common' -import { FC } from 'react' +import { FC, useEffect } from 'react' import { CellPosition, RowPosition } from './utils' import { Toolbar } from './toolbar/toolbar' import { Table } from './table' -import { SelectionContextProvider } from './contexts/selection-context' -import { EditingContextProvider } from './contexts/editing-context' +import { + SelectionContextProvider, + useSelectionContext, +} from './contexts/selection-context' +import { + EditingContextProvider, + useEditingContext, +} from './contexts/editing-context' import { EditorView } from '@codemirror/view' import { ErrorBoundary } from 'react-error-boundary' import { Alert, Button } from 'react-bootstrap' import { EditorSelection } from '@codemirror/state' import { CodeMirrorViewContextProvider } from '../codemirror-editor' import { TableProvider } from './contexts/table-context' +import { TabularProvider, useTabularContext } from './contexts/tabular-context' export type CellData = { // TODO: Add columnSpan @@ -72,17 +79,45 @@ export const Tabular: FC<{ onError={(error, componentStack) => console.error(error, componentStack)} > - - - -
- - - - - - + + + + + + + + + ) } + +const TabularWrapper: FC = () => { + const { setSelection, selection } = useSelectionContext() + const { commitCellData, cellData } = useEditingContext() + const { ref } = useTabularContext() + useEffect(() => { + const listener: (event: MouseEvent) => void = event => { + if (!ref.current?.contains(event.target as Node)) { + if (selection) { + setSelection(null) + } + if (cellData) { + commitCellData() + } + } + } + window.addEventListener('mousedown', listener) + + return () => { + window.removeEventListener('mousedown', listener) + } + }, [cellData, commitCellData, selection, setSelection, ref]) + return ( +
+ +
+ + ) +} diff --git a/services/web/frontend/js/features/source-editor/components/table-generator/toolbar/toolbar-button-menu.tsx b/services/web/frontend/js/features/source-editor/components/table-generator/toolbar/toolbar-button-menu.tsx index 073221dbdc..b1df39ae36 100644 --- a/services/web/frontend/js/features/source-editor/components/table-generator/toolbar/toolbar-button-menu.tsx +++ b/services/web/frontend/js/features/source-editor/components/table-generator/toolbar/toolbar-button-menu.tsx @@ -3,7 +3,7 @@ import useDropdown from '../../../../../shared/hooks/use-dropdown' import { Button, ListGroup, Overlay, Popover } from 'react-bootstrap' import Tooltip from '../../../../../shared/components/tooltip' import MaterialIcon from '../../../../../shared/components/material-icon' -import { useCodeMirrorViewContext } from '../../codemirror-editor' +import { useTabularContext } from '../contexts/tabular-context' export const ToolbarButtonMenu: FC<{ id: string @@ -13,7 +13,7 @@ export const ToolbarButtonMenu: FC<{ }> = memo(function ButtonMenu({ icon, id, label, children, disabled }) { const target = useRef(null) const { open, onToggle, ref } = useDropdown() - const view = useCodeMirrorViewContext() + const { ref: tableContainerRef } = useTabularContext() const button = ( ) - const overlay = open && ( + const overlay = open && tabularRef.current && ( onToggle(false)} animation={false} - container={view.dom} + container={tabularRef.current} containerPadding={0} placement="bottom" rootClose diff --git a/services/web/frontend/js/features/source-editor/components/table-generator/utils.ts b/services/web/frontend/js/features/source-editor/components/table-generator/utils.ts index c348c69dc9..4ed3f87101 100644 --- a/services/web/frontend/js/features/source-editor/components/table-generator/utils.ts +++ b/services/web/frontend/js/features/source-editor/components/table-generator/utils.ts @@ -178,6 +178,11 @@ function parseTabularBody( if (lastCell?.content) { throw new Error('\\hline must be at the start of a row') } + // push start of cell past the hline + if (lastCell) { + lastCell.position.from = currentChild.to + lastCell.position.to = currentChild.to + } const lastRow = getLastRow() lastRow.hlines.push({ position: { from: currentChild.from, to: currentChild.to }, diff --git a/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts b/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts index d599ef72cb..d4f3d43778 100644 --- a/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts +++ b/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts @@ -260,6 +260,7 @@ export const toolbarPanel = () => [ tableLayout: 'fixed', fontSize: '6px', cursor: 'pointer', + width: '160px', '& td': { outline: '1px solid #E7E9EE', outlineOffset: '-2px', @@ -279,6 +280,7 @@ export const toolbarPanel = () => [ }, '.ol-cm-toolbar-table-grid-popover': { padding: '8px', + marginLeft: '80px', }, }), ]