mirror of
https://github.com/overleaf/overleaf.git
synced 2025-03-22 02:04:31 +00:00
Merge pull request #14233 from overleaf/mj-table-generator-skip-hlines
[visual] Table generator tweaks GitOrigin-RevId: 80ec32d024d185861a3635d5cc6d77d6a7031b64
This commit is contained in:
parent
63b09c3da3
commit
0b91a2052a
10 changed files with 204 additions and 34 deletions
|
@ -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<HTMLDivElement>(null)
|
||||
const cellRef = useRef<HTMLTableCellElement>(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,
|
||||
|
|
|
@ -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 (
|
||||
<EditingContext.Provider
|
||||
value={{
|
||||
|
@ -92,6 +110,7 @@ export const EditingContextProvider: FC = ({ children }) => {
|
|||
cancelEditing,
|
||||
commitCellData,
|
||||
startEditing,
|
||||
clearCells,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -152,6 +152,8 @@ const SelectionContext = createContext<
|
|||
| {
|
||||
selection: TableSelection | null
|
||||
setSelection: Dispatch<SetStateAction<TableSelection | null>>
|
||||
dragging: boolean
|
||||
setDragging: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
|
@ -170,8 +172,11 @@ export const useSelectionContext = () => {
|
|||
|
||||
export const SelectionContextProvider: FC = ({ children }) => {
|
||||
const [selection, setSelection] = useState<TableSelection | null>(null)
|
||||
const [dragging, setDragging] = useState(false)
|
||||
return (
|
||||
<SelectionContext.Provider value={{ selection, setSelection }}>
|
||||
<SelectionContext.Provider
|
||||
value={{ selection, setSelection, dragging, setDragging }}
|
||||
>
|
||||
{children}
|
||||
</SelectionContext.Provider>
|
||||
)
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { FC, RefObject, createContext, useContext, useRef } from 'react'
|
||||
|
||||
const TabularContext = createContext<
|
||||
{ ref: RefObject<HTMLDivElement> } | undefined
|
||||
>(undefined)
|
||||
|
||||
export const TabularProvider: FC = ({ children }) => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
return (
|
||||
<TabularContext.Provider value={{ ref }}>
|
||||
{children}
|
||||
</TabularContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useTabularContext = () => {
|
||||
const tabularContext = useContext(TabularContext)
|
||||
if (!tabularContext) {
|
||||
throw new Error('TabularContext must be used within TabularProvider')
|
||||
}
|
||||
return tabularContext
|
||||
}
|
|
@ -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<HTMLTableElement>(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 (
|
||||
|
|
|
@ -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)}
|
||||
>
|
||||
<CodeMirrorViewContextProvider value={view}>
|
||||
<TableProvider tabularNode={tabularNode} view={view}>
|
||||
<SelectionContextProvider>
|
||||
<EditingContextProvider>
|
||||
<div className="table-generator">
|
||||
<Toolbar />
|
||||
<Table />
|
||||
</div>
|
||||
</EditingContextProvider>
|
||||
</SelectionContextProvider>
|
||||
</TableProvider>
|
||||
<TabularProvider>
|
||||
<TableProvider tabularNode={tabularNode} view={view}>
|
||||
<SelectionContextProvider>
|
||||
<EditingContextProvider>
|
||||
<TabularWrapper />
|
||||
</EditingContextProvider>
|
||||
</SelectionContextProvider>
|
||||
</TableProvider>
|
||||
</TabularProvider>
|
||||
</CodeMirrorViewContextProvider>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="table-generator" ref={ref}>
|
||||
<Toolbar />
|
||||
<Table />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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<any>(null)
|
||||
const { open, onToggle, ref } = useDropdown()
|
||||
const view = useCodeMirrorViewContext()
|
||||
const { ref: tableContainerRef } = useTabularContext()
|
||||
|
||||
const button = (
|
||||
<Button
|
||||
|
@ -36,12 +36,12 @@ export const ToolbarButtonMenu: FC<{
|
|||
</Button>
|
||||
)
|
||||
|
||||
const overlay = (
|
||||
const overlay = tableContainerRef.current && (
|
||||
<Overlay
|
||||
show={open}
|
||||
target={target.current}
|
||||
placement="bottom"
|
||||
container={view.dom}
|
||||
container={tableContainerRef.current}
|
||||
containerPadding={0}
|
||||
animation
|
||||
onHide={() => onToggle(false)}
|
||||
|
|
|
@ -3,7 +3,7 @@ import useDropdown from '../../../../../shared/hooks/use-dropdown'
|
|||
import { Overlay, Popover } from 'react-bootstrap'
|
||||
import MaterialIcon from '../../../../../shared/components/material-icon'
|
||||
import Tooltip from '../../../../../shared/components/tooltip'
|
||||
import { useCodeMirrorViewContext } from '../../codemirror-editor'
|
||||
import { useTabularContext } from '../contexts/tabular-context'
|
||||
|
||||
export const ToolbarDropdown: FC<{
|
||||
id: string
|
||||
|
@ -23,7 +23,7 @@ export const ToolbarDropdown: FC<{
|
|||
}) => {
|
||||
const { open, onToggle } = useDropdown()
|
||||
const toggleButtonRef = useRef<HTMLButtonElement | null>(null)
|
||||
const view = useCodeMirrorViewContext()
|
||||
const { ref: tabularRef } = useTabularContext()
|
||||
|
||||
const button = (
|
||||
<button
|
||||
|
@ -42,12 +42,12 @@ export const ToolbarDropdown: FC<{
|
|||
<MaterialIcon type={icon} />
|
||||
</button>
|
||||
)
|
||||
const overlay = open && (
|
||||
const overlay = open && tabularRef.current && (
|
||||
<Overlay
|
||||
show
|
||||
onHide={() => onToggle(false)}
|
||||
animation={false}
|
||||
container={view.dom}
|
||||
container={tabularRef.current}
|
||||
containerPadding={0}
|
||||
placement="bottom"
|
||||
rootClose
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
|
Loading…
Reference in a new issue