Merge pull request #14317 from overleaf/mj-table-gen-update

[visual] in-place table generator updates

GitOrigin-RevId: 410411fd9158e6c8c6fe6a5300556800732a252a
This commit is contained in:
Mathias Jakobsen 2023-08-17 09:09:54 +01:00 committed by Copybot
parent 33ac9e18f9
commit 2d15ce8d05
8 changed files with 85 additions and 24 deletions

View file

@ -118,7 +118,7 @@ export const Cell: FC<{
if (renderDiv.current && !editing) {
const tree = parser.parse(toDisplay)
const node = tree.topNode
renderDiv.current.innerText = ''
typesetNodeIntoElement(
node,
renderDiv.current,

View file

@ -166,6 +166,16 @@ export class TableSelection {
maxY === totalRows - 1
)
}
width() {
const { minX, maxX } = this.normalized()
return maxX - minX + 1
}
height() {
const { minY, maxY } = this.normalized()
return maxY - minY + 1
}
}
const SelectionContext = createContext<

View file

@ -1,5 +1,5 @@
import { FC, createContext, useContext } from 'react'
import { Positions, TableData } from '../tabular'
import { Positions, TableData, TableRenderingError } from '../tabular'
import {
CellPosition,
CellSeparator,
@ -41,7 +41,7 @@ export const TableProvider: FC<{
// TODO: Validate better that the table matches the column definition
for (const row of tableData.table.rows) {
if (row.cells.length !== tableData.table.columns.length) {
throw new Error("Table doesn't match column definition")
return <TableRenderingError view={view} codePosition={tabularNode.from} />
}
}

View file

@ -173,8 +173,15 @@ export const removeRowOrColumns = (
const numberOfRows = positions.rowPositions.length
if (selection.spansEntireTable(numberOfColumns, numberOfRows)) {
return emptyTable(view, columnSpecification, positions)
emptyTable(view, columnSpecification, positions)
return new TableSelection({ cell: 0, row: 0 })
}
const removedRows =
Number(selection.isRowSelected(startRow, numberOfColumns)) *
selection.height()
const removedColumns =
Number(selection.isColumnSelected(startCell, numberOfRows)) *
selection.width()
for (let row = startRow; row <= endRow; row++) {
if (selection.isRowSelected(row, numberOfColumns)) {
@ -221,6 +228,13 @@ export const removeRowOrColumns = (
insert: newSpecification,
})
view.dispatch({ changes })
const updatedNumberOfRows = numberOfRows - removedRows
const updatedNumberOfColumns = numberOfColumns - removedColumns
// Clamp selection to new table size
return new TableSelection({
cell: Math.max(0, Math.min(updatedNumberOfColumns - 1, startCell)),
row: Math.max(0, Math.min(updatedNumberOfRows - 1, startRow)),
})
}
const emptyTable = (
@ -262,6 +276,13 @@ export const insertRow = (
const numberOfColumns = positions.cells[maxY].length
const insert = `\n${' &'.repeat(numberOfColumns - 1)}\\\\`
view.dispatch({ changes: { from, to: from, insert } })
if (!below) {
return selection
}
return new TableSelection(
{ cell: 0, row: maxY + 1 },
{ cell: numberOfColumns - 1, row: maxY + 1 }
)
}
export const insertColumn = (
@ -300,6 +321,13 @@ export const insertColumn = (
insert: generateColumnSpecification(columnSpecification),
})
view.dispatch({ changes })
if (!after) {
return selection
}
return new TableSelection(
{ cell: maxX + 1, row: 0 },
{ cell: maxX + 1, row: positions.rowPositions.length - 1 }
)
}
export const removeNodes = (

View file

@ -36,7 +36,6 @@ export const ToolbarButton = memo<{
if (command) {
event.preventDefault()
command(view)
view.focus()
}
},
[command, view]

View file

@ -23,10 +23,9 @@ export const ToolbarDropdown: FC<{
disabled,
disabledTooltip,
}) => {
const { open, onToggle } = useDropdown()
const { open, onToggle, ref } = useDropdown()
const toggleButtonRef = useRef<HTMLButtonElement | null>(null)
const { ref: tabularRef } = useTabularContext()
const button = (
<button
ref={toggleButtonRef}
@ -34,8 +33,13 @@ export const ToolbarDropdown: FC<{
id={id}
aria-haspopup="true"
className={btnClassName}
onMouseDown={event => event.preventDefault()}
onClick={() => onToggle(!open)}
onMouseDown={event => {
event.preventDefault()
event.stopPropagation()
}}
onClick={() => {
onToggle(!open)
}}
aria-label={tooltip}
disabled={disabled}
aria-disabled={disabled}
@ -44,26 +48,30 @@ export const ToolbarDropdown: FC<{
<MaterialIcon type={icon} />
</button>
)
const overlay = open && tabularRef.current && (
const overlay = tabularRef.current && (
<Overlay
show
onHide={() => onToggle(false)}
animation={false}
container={tabularRef.current}
containerPadding={0}
placement="bottom"
rootClose
show={open}
target={toggleButtonRef.current ?? undefined}
placement="bottom"
container={tabularRef.current}
animation
containerPadding={0}
onHide={() => onToggle(false)}
>
<Popover
id={`${id}-popover`}
ref={ref}
className="table-generator-toolbar-dropdown-popover"
>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions,
jsx-a11y/click-events-have-key-events */}
<div
className="table-generator-toolbar-dropdown-menu"
id={`${id}-menu`}
role="menu"
aria-labelledby={id}
onClick={() => {
onToggle(false)
}}
>
{children}
</div>
@ -83,6 +91,7 @@ export const ToolbarDropdown: FC<{
return (
<>
<Tooltip
hidden={open}
id={id}
description={disabled && disabledTooltip ? disabledTooltip : tooltip}
overlayProps={{ placement: 'bottom' }}

View file

@ -19,7 +19,7 @@ import { useCodeMirrorViewContext } from '../../codemirror-editor'
import { useTableContext } from '../contexts/table-context'
export const Toolbar = memo(function Toolbar() {
const { selection } = useSelectionContext()
const { selection, setSelection } = useSelectionContext()
const view = useCodeMirrorViewContext()
const { positions, rowSeparators, cellSeparators, tableEnvironment } =
useTableContext()
@ -159,7 +159,9 @@ export const Toolbar = memo(function Toolbar() {
) && !selection.isAnyColumnSelected(positions.rowPositions.length)
}
command={() =>
removeRowOrColumns(view, selection, positions, cellSeparators)
setSelection(
removeRowOrColumns(view, selection, positions, cellSeparators)
)
}
/>
<ToolbarDropdown
@ -174,7 +176,7 @@ export const Toolbar = memo(function Toolbar() {
role="menuitem"
type="button"
onClick={() => {
insertColumn(view, selection, positions, false)
setSelection(insertColumn(view, selection, positions, false))
}}
>
<span className="table-generator-button-label">
@ -186,7 +188,7 @@ export const Toolbar = memo(function Toolbar() {
role="menuitem"
type="button"
onClick={() => {
insertColumn(view, selection, positions, true)
setSelection(insertColumn(view, selection, positions, true))
}}
>
<span className="table-generator-button-label">
@ -199,7 +201,7 @@ export const Toolbar = memo(function Toolbar() {
role="menuitem"
type="button"
onClick={() => {
insertRow(view, selection, positions, false)
setSelection(insertRow(view, selection, positions, false))
}}
>
<span className="table-generator-button-label">
@ -211,7 +213,7 @@ export const Toolbar = memo(function Toolbar() {
role="menuitem"
type="button"
onClick={() => {
insertRow(view, selection, positions, true)
setSelection(insertRow(view, selection, positions, true))
}}
>
<span className="table-generator-button-label">

View file

@ -38,6 +38,19 @@ export class TabularWidget extends WidgetType {
)
}
updateDOM(dom: HTMLElement, view: EditorView): boolean {
this.element = dom
ReactDOM.render(
<Tabular
view={view}
tabularNode={this.tabularNode}
tableNode={this.tableNode}
/>,
this.element
)
return true
}
destroy() {
console.debug('destroying tabular widget')
if (this.element) {