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 3c171067a7..4f3b0afe92 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 @@ -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< diff --git a/services/web/frontend/js/features/source-editor/components/table-generator/contexts/table-context.tsx b/services/web/frontend/js/features/source-editor/components/table-generator/contexts/table-context.tsx index b7831cf07b..59b987c7a3 100644 --- a/services/web/frontend/js/features/source-editor/components/table-generator/contexts/table-context.tsx +++ b/services/web/frontend/js/features/source-editor/components/table-generator/contexts/table-context.tsx @@ -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 diff --git a/services/web/frontend/js/features/source-editor/components/table-generator/toolbar/commands.ts b/services/web/frontend/js/features/source-editor/components/table-generator/toolbar/commands.ts index 89b7ec85f4..2da5215426 100644 --- a/services/web/frontend/js/features/source-editor/components/table-generator/toolbar/commands.ts +++ b/services/web/frontend/js/features/source-editor/components/table-generator/toolbar/commands.ts @@ -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 }) +} 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 b1df39ae36..d30ab754ea 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 @@ -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(null) const { open, onToggle, ref } = useDropdown() const { ref: tableContainerRef } = useTabularContext() const button = ( - + ) const overlay = tableContainerRef.current && ( @@ -63,21 +71,14 @@ export const ToolbarButtonMenu: FC<{ ) - if (!label) { - return ( - <> - {button} - {overlay} - - ) - } - return ( <>