I’m trying to prevent pasting tables into a table cell via transformPasted, but apparently I’m missing something. That’s my test code:
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
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.)
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:
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?
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.
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:
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
},
},
})