mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #14664 from overleaf/ae-paste-rowspan
[visual] Handle rowspan on pasted table cells GitOrigin-RevId: 53dda99ae1c300b9c4ec8f711b70b16ef66392c8
This commit is contained in:
parent
6cde5e2e90
commit
4a157e7086
2 changed files with 121 additions and 19 deletions
|
@ -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({
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue