TableKit: Pasting tables with merged cells from clipboard crashes with RangeError
Affected Packages
tiptap/extension-table
Version(s)
3.13.0
Bug Description
Bug description
Pasting a table with merged cells into an existing TipTap table crashes with
an uncaught RangeError: No cell with offset found when the bottom-right
corner of the copied selection is covered by a rowspan or colspan cell
that starts in an earlier row or column.
On Windows, an additional issue causes the crash to happen even for simpler tables (see Root cause → Issue 1 below).
Steps to reproduce
- Create a TipTap editor with
TableKitenabled and insert a table. - In any application (Google Sheets, Excel, Word), create a table where the last column of the selection ends inside a merged cell:
┌──────┬──────┬──────────┐
│ A │ B │ C │ ← C has rowspan=2
├──────┼──────┤ (spans │
│ D │ E │ 2 rows) │ ← selection ends here
└──────┴──────┴──────────┘- Copy the table and paste it into the TipTap table.
Expected behavior
The table content is pasted, preserving merged cells.
Actual behavior
Uncaught RangeError: No cell with offset 495 found
at TableMap.findCell
at TableMap.rectBetween
at new CellSelection
at insertCells
at Plugin.handlePasteThe paste fails entirely on both macOS and Windows.
Root cause
Two issues combine to cause the crash:
Issue 1 — Windows clipboard HTML wrapping (Windows only)
Google Sheets and Excel on Windows wrap clipboard HTML in custom elements:
<google-sheets-html-origin>
<style>...</style>
<table>...</table>
</google-sheets-html-origin>pastedCells(slice) expects the slice content to start with a table row.
When it receives the outer wrapper instead, it returns null, the built-in
handlePaste exits early, and the next handler crashes (Issue 2).
On macOS the same applications put a clean <table> directly in the clipboard,
so Issue 1 is skipped — but Issue 2 can still trigger depending on table structure.
Issue 2 — positionAt returns an invalid offset (all platforms)
insertCells creates a CellSelection after inserting cells:
new CellSelection(
tr.doc.resolve(tableStart + map.positionAt(top, left, table)),
tr.doc.resolve(tableStart + map.positionAt(bottom - 1, right - 1, table))
)When (bottom-1, right-1) falls inside a cell covered by rowspan or colspan
from an earlier row/column, positionAt returns rowEnd - 1 (an end-of-row
sentinel, not a valid cell offset). TableMap.findCell then throws RangeError.
Workaround
We worked around both issues with a custom extension:
- Detect clipboard HTML with
rowspan/colspan≥ 2 (including unquoted attributes from Windows:colspan=3). - Extract just the
<table>element from the clipboard HTML, discarding the outer wrapper (fixes Issue 1). - Append a temporary extra empty column to every row so that
positionAt(bottom-1, right-1)always resolves to an explicit cell (fixes Issue 2). - Call
__insertCellswith the modified slice. - Delete the extra column afterwards with
deleteColumn.
Environment
| TipTap | 3.x |
| prosemirror-tables | 1.8.1 |
| Clipboard source | Google Sheets / Microsoft Excel (Windows and macOS) |
| Browser | Chrome (Chromium) |
Minimal reproducing table
<table>
<tr><td>A</td><td>B</td><td rowspan="2">C</td></tr>
<tr><td>D</td><td>E</td></tr>
</table>Browser Used
Chrome
Code Example URL
No response
Expected Behavior
The table content is pasted, preserving merged cells.
Additional Context (Optional)
No response
Dependency Updates
- Yes, I've updated all my dependencies.
Source: ueberdosis/tiptap