Merge pull request #14664 from overleaf/ae-paste-rowspan

[visual] Handle rowspan on pasted table cells

GitOrigin-RevId: 53dda99ae1c300b9c4ec8f711b70b16ef66392c8
This commit is contained in:
Mathias Jakobsen 2023-09-06 15:05:59 +01:00 committed by Copybot
parent 6cde5e2e90
commit 4a157e7086
2 changed files with 121 additions and 19 deletions

View file

@ -229,9 +229,58 @@ const processTables = (element: HTMLElement) => {
// move the table into the container // move the table into the container
container.append(table) container.append(table)
// add empty cells to account for rowspan
for (const cell of table.querySelectorAll<HTMLTableCellElement>(
'th[rowspan],td[rowspan]'
)) {
const rowspan = Number(cell.getAttribute('rowspan') || '1')
const colspan = Number(cell.getAttribute('colspan') || '1')
let row: HTMLTableRowElement | null = cell.closest('tr')
if (row) {
let position = 0
for (const child of row.cells) {
if (child === cell) {
break
}
position += Number(child.getAttribute('colspan') || '1')
}
for (let i = 1; i < rowspan; i++) {
const nextElement: Element | null = row?.nextElementSibling
if (!isTableRow(nextElement)) {
break
}
row = nextElement
let targetCell: HTMLTableCellElement | undefined
let targetPosition = 0
for (const child of row.cells) {
if (targetPosition === position) {
targetCell = child
break
}
targetPosition += Number(child.getAttribute('colspan') || '1')
}
const fillerCells = Array.from({ length: colspan }, () =>
document.createElement('td')
)
if (targetCell) {
targetCell.before(...fillerCells)
} else {
row.append(...fillerCells)
}
}
}
}
} }
} }
const isTableRow = (element: Element | null): element is HTMLTableRowElement =>
element?.tagName === 'TR'
const cellAlignment = new Map([ const cellAlignment = new Map([
['left', 'l'], ['left', 'l'],
['center', 'c'], ['center', 'c'],
@ -370,9 +419,15 @@ const nextRowHasBorderStyle = (
} }
const startMulticolumn = (element: HTMLTableCellElement): string => { const startMulticolumn = (element: HTMLTableCellElement): string => {
const colspan = element.getAttribute('colspan') ?? 1 const colspan = Number(element.getAttribute('colspan') || 1)
const alignment = cellAlignment.get(element.style.textAlign) ?? 'l' const alignment = cellAlignment.get(element.style.textAlign) ?? 'l'
return `\\multicolumn{${Number(colspan)}}{${alignment}}{` return `\\multicolumn{${colspan}}{${alignment}}{`
}
const startMultirow = (element: HTMLTableCellElement): string => {
const rowspan = Number(element.getAttribute('rowspan') || 1)
// NOTE: it would be useful to read cell width if specified, using `*` as a starting point
return `\\multirow{${rowspan}}{*}{`
} }
const selectors = [ const selectors = [
@ -539,25 +594,30 @@ const selectors = [
}, },
}), }),
createSelector({ createSelector({
selector: 'tr > td:not(:last-child), tr > th:not(:last-child)', selector: 'tr > td, tr > th',
start: (element: HTMLTableCellElement) => { start: (element: HTMLTableCellElement) => {
const colspan = element.getAttribute('colspan') let output = ''
return colspan ? startMulticolumn(element) : '' if (element.getAttribute('colspan')) {
output += startMulticolumn(element)
}
// NOTE: multirow is nested inside multicolumn
if (element.getAttribute('rowspan')) {
output += startMultirow(element)
}
return output
}, },
end: element => { end: element => {
const colspan = element.getAttribute('colspan') let output = ''
return colspan ? `} & ` : ` & ` // NOTE: multirow is nested inside multicolumn
}, if (element.getAttribute('rowspan')) {
}), output += '}'
createSelector({ }
selector: 'tr > td:last-child, tr > th:last-child', if (element.getAttribute('colspan')) {
start: (element: HTMLTableCellElement) => { output += '}'
const colspan = element.getAttribute('colspan') }
return colspan ? startMulticolumn(element) : '' const row = element.parentElement as HTMLTableRowElement
}, const isLastChild = row.cells.item(row.cells.length - 1) === element
end: element => { return output + (isLastChild ? ' \\\\' : ' & ')
const colspan = element.getAttribute('colspan')
return colspan ? `} \\\\` : ` \\\\`
}, },
}), }),
createSelector({ createSelector({

View file

@ -161,7 +161,7 @@ describe('<CodeMirrorEditor/> paste HTML in Visual mode', function () {
) )
}) })
it('handles a pasted table with merged cells', function () { it('handles a pasted table with merged columns', function () {
mountEditor() mountEditor()
const data = [ const data = [
@ -182,6 +182,48 @@ describe('<CodeMirrorEditor/> paste HTML in Visual mode', function () {
) )
}) })
it('handles a pasted table with merged rows', function () {
mountEditor()
const data = [
`<table><tbody>`,
`<tr><td>test</td><td>test</td><td>test</td></tr>`,
`<tr><td rowspan="2">test</td><td>test</td><td>test</td></tr>`,
`<tr><td>test</td><td>test</td></tr>`,
`</tbody></table>`,
].join('')
const clipboardData = new DataTransfer()
clipboardData.setData('text/html', data)
cy.get('@content').trigger('paste', { clipboardData })
cy.get('@content').should(
'have.text',
'\\begin{tabular}{l l l}test & test & test ↩\\multirow{2}{*}{test} & test & test ↩ & test & test ↩\\end{tabular}'
)
})
it('handles a pasted table with merged rows and columns', function () {
mountEditor()
const data = [
`<table><tbody>`,
`<tr><td colspan="2" rowspan="2">test</td><td>test</td></tr>`,
`<tr><td>test</td></tr>`,
`<tr><td>test</td><td>test</td><td>test</td></tr>`,
`</tbody></table>`,
].join('')
const clipboardData = new DataTransfer()
clipboardData.setData('text/html', data)
cy.get('@content').trigger('paste', { clipboardData })
cy.get('@content').should(
'have.text',
'\\begin{tabular}{l l l}\\multicolumn{2}{l}{\\multirow{2}{*}{test}} & test ↩ & & test ↩test & test & test ↩\\end{tabular}'
)
})
it('handles a pasted table with adjacent borders and merged cells', function () { it('handles a pasted table with adjacent borders and merged cells', function () {
mountEditor() mountEditor()