mirror of
https://github.com/overleaf/overleaf.git
synced 2025-03-22 02:04:31 +00:00
Merge pull request #14249 from overleaf/mj-delete-row-column
[visual] Enable buttons for deleting and adding columns or rows GitOrigin-RevId: 5cb68091d79b46eab507510e03180852b97666b9
This commit is contained in:
parent
31c285871a
commit
5c5c5be594
8 changed files with 246 additions and 42 deletions
|
@ -66,6 +66,16 @@ export class TableSelection {
|
|||
return row >= minY && row <= maxY && minX === 0 && maxX === totalColumns - 1
|
||||
}
|
||||
|
||||
isAnyRowSelected(totalColumns: number) {
|
||||
const { minX, maxX } = this.normalized()
|
||||
return minX === 0 && maxX === totalColumns - 1
|
||||
}
|
||||
|
||||
isAnyColumnSelected(totalRows: number) {
|
||||
const { minY, maxY } = this.normalized()
|
||||
return minY === 0 && maxY === totalRows - 1
|
||||
}
|
||||
|
||||
isColumnSelected(cell: number, totalRows: number) {
|
||||
const { minX, maxX, minY, maxY } = this.normalized()
|
||||
return cell >= minX && cell <= maxX && minY === 0 && maxY === totalRows - 1
|
||||
|
@ -146,6 +156,16 @@ export class TableSelection {
|
|||
{ row: newRow, cell: this.to.cell }
|
||||
)
|
||||
}
|
||||
|
||||
spansEntireTable(totalColumns: number, totalRows: number) {
|
||||
const { minX, maxX, minY, maxY } = this.normalized()
|
||||
return (
|
||||
minX === 0 &&
|
||||
maxX === totalColumns - 1 &&
|
||||
minY === 0 &&
|
||||
maxY === totalRows - 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const SelectionContext = createContext<
|
||||
|
|
|
@ -2,6 +2,7 @@ import { FC, createContext, useContext } from 'react'
|
|||
import { Positions, TableData } from '../tabular'
|
||||
import {
|
||||
CellPosition,
|
||||
CellSeparator,
|
||||
RowPosition,
|
||||
RowSeparator,
|
||||
generateTable,
|
||||
|
@ -16,6 +17,7 @@ const TableContext = createContext<
|
|||
specification: { from: number; to: number }
|
||||
rowPositions: RowPosition[]
|
||||
rowSeparators: RowSeparator[]
|
||||
cellSeparators: CellSeparator[][]
|
||||
positions: Positions
|
||||
}
|
||||
| undefined
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { EditorView } from '@codemirror/view'
|
||||
import { ColumnDefinition, Positions } from '../tabular'
|
||||
import { ChangeSpec } from '@codemirror/state'
|
||||
import { RowSeparator, parseColumnSpecifications } from '../utils'
|
||||
import {
|
||||
CellSeparator,
|
||||
RowSeparator,
|
||||
parseColumnSpecifications,
|
||||
} from '../utils'
|
||||
import { TableSelection } from '../contexts/selection-context'
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
@ -140,3 +144,154 @@ const generateColumnSpecification = (columns: ColumnDefinition[]) => {
|
|||
)
|
||||
.join('')
|
||||
}
|
||||
|
||||
export const removeRowOrColumns = (
|
||||
view: EditorView,
|
||||
selection: TableSelection,
|
||||
positions: Positions,
|
||||
cellSeparators: CellSeparator[][]
|
||||
) => {
|
||||
const {
|
||||
minX: startCell,
|
||||
maxX: endCell,
|
||||
minY: startRow,
|
||||
maxY: endRow,
|
||||
} = selection.normalized()
|
||||
const changes: { from: number; to: number; insert: string }[] = []
|
||||
const specification = view.state.sliceDoc(
|
||||
positions.columnDeclarations.from,
|
||||
positions.columnDeclarations.to
|
||||
)
|
||||
const columnSpecification = parseColumnSpecifications(specification)
|
||||
const numberOfColumns = columnSpecification.length
|
||||
const numberOfRows = positions.rowPositions.length
|
||||
|
||||
if (selection.spansEntireTable(numberOfColumns, numberOfRows)) {
|
||||
return emptyTable(view, columnSpecification, positions)
|
||||
}
|
||||
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
if (selection.isRowSelected(row, numberOfColumns)) {
|
||||
const rowPosition = positions.rowPositions[row]
|
||||
changes.push({
|
||||
from: rowPosition.from,
|
||||
to: rowPosition.to,
|
||||
insert: '',
|
||||
})
|
||||
} else {
|
||||
for (let cell = startCell; cell <= endCell; cell++) {
|
||||
if (selection.isColumnSelected(cell, numberOfRows)) {
|
||||
// FIXME: handle multicolumn
|
||||
if (cell === 0 && cellSeparators[row].length > 0) {
|
||||
// Remove the cell separator between the first and second cell
|
||||
changes.push({
|
||||
from: positions.cells[row][cell].from,
|
||||
to: cellSeparators[row][0].to,
|
||||
insert: '',
|
||||
})
|
||||
} else {
|
||||
// Remove the cell separator between the cell before and this if possible
|
||||
const cellPosition = positions.cells[row][cell]
|
||||
const from =
|
||||
cellSeparators[row][cell - 1]?.from ?? cellPosition.from
|
||||
const to = cellPosition.to
|
||||
changes.push({
|
||||
from,
|
||||
to,
|
||||
insert: '',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const filteredColumns = columnSpecification.filter(
|
||||
(_, i) => !selection.isColumnSelected(i, numberOfRows)
|
||||
)
|
||||
const newSpecification = generateColumnSpecification(filteredColumns)
|
||||
changes.push({
|
||||
from: positions.columnDeclarations.from,
|
||||
to: positions.columnDeclarations.to,
|
||||
insert: newSpecification,
|
||||
})
|
||||
view.dispatch({ changes })
|
||||
}
|
||||
|
||||
const emptyTable = (
|
||||
view: EditorView,
|
||||
columnSpecification: ColumnDefinition[],
|
||||
positions: Positions
|
||||
) => {
|
||||
const newColumns = columnSpecification.slice(0, 1)
|
||||
newColumns[0].borderLeft = 0
|
||||
newColumns[0].borderRight = 0
|
||||
const newSpecification = generateColumnSpecification(newColumns)
|
||||
const changes: ChangeSpec[] = []
|
||||
changes.push({
|
||||
from: positions.columnDeclarations.from,
|
||||
to: positions.columnDeclarations.to,
|
||||
insert: newSpecification,
|
||||
})
|
||||
const from = positions.rowPositions[0].from
|
||||
const to = positions.rowPositions[positions.rowPositions.length - 1].to
|
||||
changes.push({
|
||||
from,
|
||||
to,
|
||||
insert: '\\\\',
|
||||
})
|
||||
view.dispatch({ changes })
|
||||
}
|
||||
|
||||
export const insertRow = (
|
||||
view: EditorView,
|
||||
selection: TableSelection,
|
||||
positions: Positions,
|
||||
below: boolean
|
||||
) => {
|
||||
// TODO: Handle borders
|
||||
const { maxY, minY } = selection.normalized()
|
||||
const from = below
|
||||
? positions.rowPositions[maxY].to
|
||||
: positions.rowPositions[minY].from
|
||||
const numberOfColumns = positions.cells[maxY].length
|
||||
const insert = `\n${' &'.repeat(numberOfColumns - 1)}\\\\`
|
||||
view.dispatch({ changes: { from, to: from, insert } })
|
||||
}
|
||||
|
||||
export const insertColumn = (
|
||||
view: EditorView,
|
||||
selection: TableSelection,
|
||||
positions: Positions,
|
||||
after: boolean
|
||||
) => {
|
||||
// TODO: Handle borders
|
||||
// FIXME: Handle multicolumn
|
||||
const { maxX, minX } = selection.normalized()
|
||||
const changes: ChangeSpec[] = []
|
||||
for (const row of positions.cells) {
|
||||
const from = after ? row[maxX].to : row[minX].from
|
||||
changes.push({
|
||||
from,
|
||||
to: from,
|
||||
insert: ' &',
|
||||
})
|
||||
}
|
||||
|
||||
const specification = view.state.sliceDoc(
|
||||
positions.columnDeclarations.from,
|
||||
positions.columnDeclarations.to
|
||||
)
|
||||
const columnSpecification = parseColumnSpecifications(specification)
|
||||
columnSpecification.splice(after ? maxX + 1 : minX, 0, {
|
||||
alignment: 'left',
|
||||
borderLeft: 0,
|
||||
borderRight: 0,
|
||||
content: 'l',
|
||||
})
|
||||
changes.push({
|
||||
from: positions.columnDeclarations.from,
|
||||
to: positions.columnDeclarations.to,
|
||||
insert: generateColumnSpecification(columnSpecification),
|
||||
})
|
||||
view.dispatch({ changes })
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { FC, memo, useRef } from 'react'
|
||||
import useDropdown from '../../../../../shared/hooks/use-dropdown'
|
||||
import { Button, ListGroup, Overlay, Popover } from 'react-bootstrap'
|
||||
import { ListGroup, Overlay, Popover } from 'react-bootstrap'
|
||||
import Tooltip from '../../../../../shared/components/tooltip'
|
||||
import MaterialIcon from '../../../../../shared/components/material-icon'
|
||||
import { useTabularContext } from '../contexts/tabular-context'
|
||||
|
@ -10,17 +10,24 @@ export const ToolbarButtonMenu: FC<{
|
|||
label: string
|
||||
icon: string
|
||||
disabled?: boolean
|
||||
}> = memo(function ButtonMenu({ icon, id, label, children, disabled }) {
|
||||
disabledLabel?: string
|
||||
}> = memo(function ButtonMenu({
|
||||
icon,
|
||||
id,
|
||||
label,
|
||||
children,
|
||||
disabled,
|
||||
disabledLabel,
|
||||
}) {
|
||||
const target = useRef<any>(null)
|
||||
const { open, onToggle, ref } = useDropdown()
|
||||
const { ref: tableContainerRef } = useTabularContext()
|
||||
|
||||
const button = (
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
className="table-generator-toolbar-button table-generator-toolbar-button-menu"
|
||||
aria-label={label}
|
||||
bsStyle={null}
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
@ -29,11 +36,12 @@ export const ToolbarButtonMenu: FC<{
|
|||
onToggle(!open)
|
||||
}}
|
||||
disabled={disabled}
|
||||
aria-disabled={disabled}
|
||||
ref={target}
|
||||
>
|
||||
<MaterialIcon type={icon} />
|
||||
<MaterialIcon type="expand_more" />
|
||||
</Button>
|
||||
</button>
|
||||
)
|
||||
|
||||
const overlay = tableContainerRef.current && (
|
||||
|
@ -63,21 +71,14 @@ export const ToolbarButtonMenu: FC<{
|
|||
</Overlay>
|
||||
)
|
||||
|
||||
if (!label) {
|
||||
return (
|
||||
<>
|
||||
{button}
|
||||
{overlay}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
hidden={open}
|
||||
id={id}
|
||||
description={<div>{label}</div>}
|
||||
description={
|
||||
<div>{disabled && disabledLabel ? disabledLabel : label}</div>
|
||||
}
|
||||
overlayProps={{ placement: 'bottom' }}
|
||||
>
|
||||
{button}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { EditorView } from '@codemirror/view'
|
||||
import classNames from 'classnames'
|
||||
import { memo, useCallback } from 'react'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import Tooltip from '../../../../../shared/components/tooltip'
|
||||
import MaterialIcon from '../../../../../shared/components/material-icon'
|
||||
import { useCodeMirrorViewContext } from '../../codemirror-editor'
|
||||
|
@ -13,9 +12,9 @@ export const ToolbarButton = memo<{
|
|||
command?: (view: EditorView) => void
|
||||
active?: boolean
|
||||
disabled?: boolean
|
||||
disabledLabel?: string
|
||||
icon: string
|
||||
hidden?: boolean
|
||||
shortcut?: string
|
||||
}>(function ToolbarButton({
|
||||
id,
|
||||
className,
|
||||
|
@ -25,7 +24,7 @@ export const ToolbarButton = memo<{
|
|||
disabled,
|
||||
icon,
|
||||
hidden = false,
|
||||
shortcut,
|
||||
disabledLabel,
|
||||
}) {
|
||||
const view = useCodeMirrorViewContext()
|
||||
const handleMouseDown = useCallback(event => {
|
||||
|
@ -44,32 +43,24 @@ export const ToolbarButton = memo<{
|
|||
)
|
||||
|
||||
const button = (
|
||||
<Button
|
||||
<button
|
||||
className={classNames('table-generator-toolbar-button', className, {
|
||||
hidden,
|
||||
active,
|
||||
})}
|
||||
aria-label={label}
|
||||
onMouseDown={handleMouseDown}
|
||||
onClick={!disabled ? handleClick : undefined}
|
||||
bsStyle={null}
|
||||
active={active}
|
||||
disabled={disabled}
|
||||
aria-disabled={disabled}
|
||||
type="button"
|
||||
>
|
||||
<MaterialIcon type={icon} />
|
||||
</Button>
|
||||
</button>
|
||||
)
|
||||
|
||||
if (!label) {
|
||||
return button
|
||||
}
|
||||
|
||||
const description = (
|
||||
<>
|
||||
<div>{label}</div>
|
||||
{shortcut && <div>{shortcut}</div>}
|
||||
</>
|
||||
)
|
||||
const description =
|
||||
disabled && disabledLabel ? <div>{disabledLabel}</div> : <div>{label}</div>
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
|
|
|
@ -12,6 +12,7 @@ export const ToolbarDropdown: FC<{
|
|||
icon?: string
|
||||
tooltip?: string
|
||||
disabled?: boolean
|
||||
disabledTooltip?: string
|
||||
}> = ({
|
||||
id,
|
||||
label,
|
||||
|
@ -20,6 +21,7 @@ export const ToolbarDropdown: FC<{
|
|||
icon = 'expand_more',
|
||||
tooltip,
|
||||
disabled,
|
||||
disabledTooltip,
|
||||
}) => {
|
||||
const { open, onToggle } = useDropdown()
|
||||
const toggleButtonRef = useRef<HTMLButtonElement | null>(null)
|
||||
|
@ -81,8 +83,8 @@ export const ToolbarDropdown: FC<{
|
|||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
id={`${id}-tooltip`}
|
||||
description={tooltip}
|
||||
id={id}
|
||||
description={disabled && disabledTooltip ? disabledTooltip : tooltip}
|
||||
overlayProps={{ placement: 'bottom' }}
|
||||
>
|
||||
{button}
|
||||
|
|
|
@ -4,14 +4,21 @@ import { ToolbarButton } from './toolbar-button'
|
|||
import { ToolbarButtonMenu } from './toolbar-button-menu'
|
||||
import { ToolbarDropdown } from './toolbar-dropdown'
|
||||
import MaterialIcon from '../../../../../shared/components/material-icon'
|
||||
import { BorderTheme, setAlignment, setBorders } from './commands'
|
||||
import {
|
||||
BorderTheme,
|
||||
insertColumn,
|
||||
insertRow,
|
||||
removeRowOrColumns,
|
||||
setAlignment,
|
||||
setBorders,
|
||||
} from './commands'
|
||||
import { useCodeMirrorViewContext } from '../../codemirror-editor'
|
||||
import { useTableContext } from '../contexts/table-context'
|
||||
|
||||
export const Toolbar = memo(function Toolbar() {
|
||||
const { selection } = useSelectionContext()
|
||||
const view = useCodeMirrorViewContext()
|
||||
const { positions, rowSeparators } = useTableContext()
|
||||
const { positions, rowSeparators, cellSeparators } = useTableContext()
|
||||
if (!selection) {
|
||||
return null
|
||||
}
|
||||
|
@ -87,6 +94,7 @@ export const Toolbar = memo(function Toolbar() {
|
|||
label="Alignment"
|
||||
icon="format_align_left"
|
||||
id="table-generator-align-dropdown"
|
||||
disabledLabel="Select a column or a merged cell to align"
|
||||
disabled={
|
||||
!selection.isColumnSelected(
|
||||
selection.from.cell,
|
||||
|
@ -124,24 +132,37 @@ export const Toolbar = memo(function Toolbar() {
|
|||
id="table-generator-merge-cells"
|
||||
label="Merge cells"
|
||||
disabled
|
||||
disabledLabel="Select cells in a row to merge"
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon="delete"
|
||||
id="table-generator-remove-column-row"
|
||||
label="Remove row or column"
|
||||
disabled
|
||||
label="Delete row or column"
|
||||
disabledLabel="Select a row or a column to delete"
|
||||
disabled={
|
||||
!(
|
||||
positions.cells.length &&
|
||||
selection.isAnyRowSelected(positions.cells[0].length)
|
||||
) && !selection.isAnyColumnSelected(positions.rowPositions.length)
|
||||
}
|
||||
command={() =>
|
||||
removeRowOrColumns(view, selection, positions, cellSeparators)
|
||||
}
|
||||
/>
|
||||
<ToolbarDropdown
|
||||
id="table-generator-add-dropdown"
|
||||
btnClassName="table-generator-toolbar-button"
|
||||
icon="add"
|
||||
tooltip="Insert"
|
||||
disabled
|
||||
disabled={!selection}
|
||||
>
|
||||
<button
|
||||
className="ol-cm-toolbar-menu-item"
|
||||
role="menuitem"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
insertColumn(view, selection, positions, false)
|
||||
}}
|
||||
>
|
||||
<span className="table-generator-button-label">
|
||||
Insert column left
|
||||
|
@ -151,6 +172,9 @@ export const Toolbar = memo(function Toolbar() {
|
|||
className="ol-cm-toolbar-menu-item"
|
||||
role="menuitem"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
insertColumn(view, selection, positions, true)
|
||||
}}
|
||||
>
|
||||
<span className="table-generator-button-label">
|
||||
Insert column right
|
||||
|
@ -161,6 +185,9 @@ export const Toolbar = memo(function Toolbar() {
|
|||
className="ol-cm-toolbar-menu-item"
|
||||
role="menuitem"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
insertRow(view, selection, positions, false)
|
||||
}}
|
||||
>
|
||||
<span className="table-generator-button-label">
|
||||
Insert row above
|
||||
|
@ -170,6 +197,9 @@ export const Toolbar = memo(function Toolbar() {
|
|||
className="ol-cm-toolbar-menu-item"
|
||||
role="menuitem"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
insertRow(view, selection, positions, true)
|
||||
}}
|
||||
>
|
||||
<span className="table-generator-button-label">
|
||||
Insert row below
|
||||
|
@ -181,7 +211,7 @@ export const Toolbar = memo(function Toolbar() {
|
|||
<ToolbarButton
|
||||
icon="delete_forever"
|
||||
id="table-generator-remove-table"
|
||||
label="Remove table"
|
||||
label="Delete table"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -99,7 +99,7 @@ type ParsedCell = {
|
|||
position: Position
|
||||
}
|
||||
|
||||
type CellSeparator = Position
|
||||
export type CellSeparator = Position
|
||||
export type RowSeparator = Position
|
||||
|
||||
type ParsedRow = {
|
||||
|
@ -233,6 +233,7 @@ export function generateTable(
|
|||
specification: { from: number; to: number }
|
||||
rowPositions: RowPosition[]
|
||||
rowSeparators: RowSeparator[]
|
||||
cellSeparators: CellSeparator[][]
|
||||
} {
|
||||
const specification = node
|
||||
.getChild('BeginEnv')
|
||||
|
@ -253,6 +254,7 @@ export function generateTable(
|
|||
const cellPositions = tableData.rows.map(row =>
|
||||
row.cells.map(cell => cell.position)
|
||||
)
|
||||
const cellSeparators = tableData.rows.map(row => row.cellSeparators)
|
||||
const rowPositions = tableData.rows.map(row => ({
|
||||
...row.position,
|
||||
hlines: row.hlines.map(hline => hline.position),
|
||||
|
@ -274,5 +276,6 @@ export function generateTable(
|
|||
specification,
|
||||
rowPositions,
|
||||
rowSeparators: tableData.rowSeparators,
|
||||
cellSeparators,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue