소스 검색

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

[visual] Handle rowspan on pasted table cells

GitOrigin-RevId: 53dda99ae1c300b9c4ec8f711b70b16ef66392c8
Mathias Jakobsen 3 년 전
부모
커밋
4a157e7086

+ 78 - 18
services/web/frontend/js/features/source-editor/extensions/visual/paste-html.ts

@@ -229,9 +229,58 @@ const processTables = (element: HTMLElement) => {
 
     // move the table into the container
     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([
   ['left', 'l'],
   ['center', 'c'],
@@ -370,9 +419,15 @@ const nextRowHasBorderStyle = (
 }
 
 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'
-  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 = [
@@ -539,25 +594,30 @@ const selectors = [
     },
   }),
   createSelector({
-    selector: 'tr > td:not(:last-child), tr > th:not(:last-child)',
+    selector: 'tr > td, tr > th',
     start: (element: HTMLTableCellElement) => {
-      const colspan = element.getAttribute('colspan')
-      return colspan ? startMulticolumn(element) : ''
-    },
-    end: element => {
-      const colspan = element.getAttribute('colspan')
-      return colspan ? `} & ` : ` & `
-    },
-  }),
-  createSelector({
-    selector: 'tr > td:last-child, tr > th:last-child',
-    start: (element: HTMLTableCellElement) => {
-      const colspan = element.getAttribute('colspan')
-      return colspan ? startMulticolumn(element) : ''
+      let output = ''
+      if (element.getAttribute('colspan')) {
+        output += startMulticolumn(element)
+      }
+      // NOTE: multirow is nested inside multicolumn
+      if (element.getAttribute('rowspan')) {
+        output += startMultirow(element)
+      }
+      return output
     },
     end: element => {
-      const colspan = element.getAttribute('colspan')
-      return colspan ? `} \\\\` : ` \\\\`
+      let output = ''
+      // NOTE: multirow is nested inside multicolumn
+      if (element.getAttribute('rowspan')) {
+        output += '}'
+      }
+      if (element.getAttribute('colspan')) {
+        output += '}'
+      }
+      const row = element.parentElement as HTMLTableRowElement
+      const isLastChild = row.cells.item(row.cells.length - 1) === element
+      return output + (isLastChild ? ' \\\\' : ' & ')
     },
   }),
   createSelector({

+ 43 - 1
services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-paste-html.spec.tsx

@@ -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()
 
     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 () {
     mountEditor()