# Trying to prevent nested table paste via \`transformPasted\`

**URL:** <https://discuss.prosemirror.net/t/trying-to-prevent-nested-table-paste-via-transformpasted/8878>\
**Category:** Uncategorized\
**Created:** [November 27, 2025, 1:26pm UTC](https://discuss.prosemirror.net/t/trying-to-prevent-nested-table-paste-via-transformpasted/8878 "2025-11-27T13:26:33Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![mejo](https://discuss.prosemirror.net/letter_avatar_proxy/v4/letter/m/f1d935/32.png) [@mejo](https://discuss.prosemirror.net/u/mejo)\
**Post date:** [November 27, 2025, 1:26pm UTC](https://discuss.prosemirror.net/t/trying-to-prevent-nested-table-paste-via-transformpasted/8878/1 "2025-11-27T13:26:33Z")

</div>

Dear all,

I’m trying to prevent pasting tables into a table cell via `transformPasted`, but apparently I’m missing something. That’s my test code:

```javascript
transformPasted: (slice, view) => {
	if (!this.editor.isActive(this.type.name)) {
		return slice
	}

	const { schema } = view.state

	let tablePaste = false
	slice.content.descendants((node) => {
		if (node.type.name === 'table') {
			tablePaste = true
		}
	})

	if (!tablePaste) {
		return slice
	}

	const textNode = schema.text('Nested tables are forbidden')
	const fragment = Fragment.from(schema.nodes.paragraph.create(null, textNode))
	const newSlice = new Slice(fragment, slice.openStart, slice.openEnd)
	return newSlice
}

```

The detection seems to work as expected. When I step through with the debugger, `tablePaste` gets set to `true` and `newSlice` gets compiled. But still, when I paste a table into a table cell, original clipboard content with the table ends up in the cell, not my new paragraph with text node.

Am I missing something? Could this be a race condition with some other paste handler taking precedence? I don’t understand why the code gets triggered when I place a breakpoint in it but it doesn’t seem to have any effect 🤔

---

<div class="post-metadata">

**Author:** ![marijn](https://discuss.prosemirror.net/user_avatar/discuss.prosemirror.net/marijn/32/15_2.png) [@marijn](https://discuss.prosemirror.net/u/marijn)\
**Post date:** [November 27, 2025, 2:07pm UTC](https://discuss.prosemirror.net/t/trying-to-prevent-nested-table-paste-via-transformpasted/8878/2 "2025-11-27T14:07:12Z")

</div>

That’s very odd. Can you reduce this to a small script so that I can debug it in isolation? (Or sometimes just making something happen in a minimal context already shows where the problem lies.)

---

<div class="post-metadata">

**Author:** ![mejo](https://discuss.prosemirror.net/letter_avatar_proxy/v4/letter/m/f1d935/32.png) [@mejo](https://discuss.prosemirror.net/u/mejo)\
**Post date:** [November 27, 2025, 3:06pm UTC](https://discuss.prosemirror.net/t/trying-to-prevent-nested-table-paste-via-transformpasted/8878/3 "2025-11-27T15:06:36Z")

</div>

Thanks @marijn! I put together a quick a codesandbox example: [https://codesandbox.io/p/devbox/cocky-snow-9f2pnk?workspaceId=ws\_KJTzr2vVN6caQzpyryFdn1](https://codesandbox.io/p/devbox/cocky-snow-9f2pnk?workspaceId=ws_KJTzr2vVN6caQzpyryFdn1)

Playing around with it a bit more, it seems like passing `slice.openStart` and `slice.openEnd` to `new Slice()` is the culprit. If I pass `0` for both instead, it seems to work as expected.

* * *

My original code (with `openStart` and `openEnd`) came from an attempt to just strip table nodes from the given `slice` and replace them with a paragraph containing the `textContent` of the original `table` node.

My understanding is that `slice.content.descendants()` will not work here as I need to traverse the whole node tree of the slice and build a new one from it, right? So what I did was recursively running traversing the nodes and their children via `forEach()` the following way:

```javascript
const filterFragment = (fragment) => {
	const nodes = []
	fragment.forEach((node) => {
		if (node.isText) {
			list.push(node)
			return
		}
		if (node.type.name === 'table') {
			console.debug('transformPasted table node', node)
			const textNode = node.textContent
				? schema.text(node.textContent)
				: null
			nodes.push(schema.nodes.paragraph.create(null, textNode))
			return
		}
		nodes.push(node.copy(filterFragment(node.content)))
	})

	return Fragment.from(nodes)
}

let tablePaste = false
slice.content.descendants((node) => {
	if (node.type.name === 'table') {
		tablePaste = true
	}
})

if (!tablePaste) {
	return slice
}

const newSlice = new Slice(filterFragment(slice.content), slice.openStart, slice.openEnd)
return newSlice

```

Stepping through with the debugger looked promising, but I guess then here the problem might as well have been that `slice.openStart` and `slice.openEnd` were wrong. Is there a smart way to calculate the correct `openStart` and `openEnd` values for the new Slice object given the compiled fragment?

---

<div class="post-metadata">

**Author:** ![marijn](https://discuss.prosemirror.net/user_avatar/discuss.prosemirror.net/marijn/32/15_2.png) [@marijn](https://discuss.prosemirror.net/u/marijn)\
**Post date:** [November 28, 2025, 2:27pm UTC](https://discuss.prosemirror.net/t/trying-to-prevent-nested-table-paste-via-transformpasted/8878/4 "2025-11-28T14:27:30Z")

</div>

I cannot access that sandbox link.

But did you see an error in the console when the problem happened? If you crash the editor’s own paste handling, you’ll get the browser’s default behavior, which would explain how you’d end up with the original content even after filtering.

---

<div class="post-metadata">

**Author:** ![mejo](https://discuss.prosemirror.net/letter_avatar_proxy/v4/letter/m/f1d935/32.png) [@mejo](https://discuss.prosemirror.net/u/mejo)\
**Post date:** [November 28, 2025, 10:04pm UTC](https://discuss.prosemirror.net/t/trying-to-prevent-nested-table-paste-via-transformpasted/8878/5 "2025-11-28T22:04:36Z")

</div>

> I cannot access that sandbox link.

Sorry, that should be fixed now.

> But did you see an error in the console when the problem happened?

Yes, indeed at least in the sandbox I see `Uncaught RangeError: Index 0 out of range for <>`.

---

<div class="post-metadata">

**Author:** ![mejo](https://discuss.prosemirror.net/letter_avatar_proxy/v4/letter/m/f1d935/32.png) [@mejo](https://discuss.prosemirror.net/u/mejo)\
**Post date:** [November 28, 2025, 11:16pm UTC](https://discuss.prosemirror.net/t/trying-to-prevent-nested-table-paste-via-transformpasted/8878/6 "2025-11-28T23:16:22Z")

</div>

I have a working implementation now that prevents nested tables and wanted to share it here. I would be curious what others think and if you see room for improvement:

```javascript
new Plugin({
	key: new PluginKey('preventNestedTables'),
	// Prevent nested table (low level protection): filter out transactions that lead to nested table
	filterTransaction: (transaction) => {
		if (!transaction.docChanged) {
			return true
		}

		let hasNestedTable = false
		transaction.doc.descendants((node, pos) => {
			if (node.type.name.startsWith('table')) {
				const $pos = transaction.doc.resolve(pos)
				for (let depth = $pos.depth; depth >= 0; depth--) {
					const ancestor = $pos.node(depth)
					if (ancestor.type.name === 'tableCell') {
						console.warn(
							'Detected nested table, filtering out transaction',
						)
						hasNestedTable = true
					}
				}
			}
		})

		return !hasNestedTable
	},
	props: {
		// Prevent nested table when pasting to a table cell
		transformPasted: (slice) => {
			if (!this.editor.isActive(this.type.name)) {
				return slice
			}

			let tablePaste = false
			slice.content.descendants((node) => {
				if (node.type.name.startsWith('table')) {
					tablePaste = true
				}
			})

			if (!tablePaste) {
				return slice
			}

			if (
				slice.content.childCount === 1
				&& slice.content.firstChild?.type.name === 'table'
			) {
				const tableChild = slice.content.firstChild.firstChild
				if (
					(tableChild.childCount === 1
						&& tableChild.type.name === 'tableRow')
					|| tableChild.type.name === 'tableHeadRow'
				) {
					const rowChild = tableChild.firstChild
					if (
						(rowChild.childCount === 1
							&& rowChild.type.name === 'tableCell')
						|| rowChild.type.name === 'tableHeader'
					) {
						return new Slice(rowChild.content, 0, 0)
					}
				}
			}

			console.warn('Nested tables are not supported')
			alert(
				t(
					'text',
					'A table was pasted into a table. Nested tables are not supported.',
				),
			)

			const newSlice = new Slice(Fragment.empty, 0, 0)
			return newSlice
		},
	},
})

```
