tiptap
tiptap copied to clipboard
Extending a Table with a NodeViewContent inserts a "div" tag where the "tbody" should be
What’s the bug you are facing?
I'm trying to make a draggable table by wrapping the table
with a drag handle (https://tiptap.dev/guide/node-views/examples#drag-handles and https://tiptap.dev/guide/node-views/react#adding-a-content-editable). I'm probably doing this all wrong but I can't seem to find the proper way to do this. Or even to wrap a Table
with another element.
It appears that by default, NodeViewContent
creates two div
tags that are nested but by using as='table'
it creates the outer as a table
. Unfortunately, according to the source code, it creates a contentDOMElement
node inside that is only ever a span
or a div
. It correctly creates the tableRow
nodes inside of that though.
Granted a table
is created and it does render on the page and it is editable but a number of the features that we need, and that make the Table
so powerful, are gone: E.g. column resizing, cell selecting, cell merging/splitting, etc.
This is what get's rendered:
<div class="react-renderer node-table">
<div class="draggable-table" intent="layoutTable" data-node-view-wrapper="" style="white-space: normal;">
<table class="content" as="table" data-node-view-content="" style="white-space: pre-wrap; min-width: 314px;">
<div style="white-space: inherit;">
<tr>
<th colspan="1" rowspan="1" colwidth="264">
<p><br class="ProseMirror-trailingBreak"></p>
</th>
<th colspan="1" rowspan="1">
<p><br class="ProseMirror-trailingBreak"></p>
</th>
<th colspan="1" rowspan="1">
<p><br class="ProseMirror-trailingBreak"></p>
</th>
</tr>
<tr>
<td colspan="1" rowspan="1" colwidth="264">
<p><br class="ProseMirror-trailingBreak"></p>
</td>
<td colspan="1" rowspan="1">
<p><br class="ProseMirror-trailingBreak"></p>
</td>
<td colspan="1" rowspan="1">
<p><br class="ProseMirror-trailingBreak"></p>
</td>
</tr>
<tr>
<td colspan="1" rowspan="1" colwidth="264">
<p><br class="ProseMirror-trailingBreak"></p>
</td>
<td colspan="1" rowspan="1">
<p><br class="ProseMirror-trailingBreak"></p>
</td>
<td colspan="1" rowspan="1">
<p><br class="ProseMirror-trailingBreak"></p>
</td>
</tr>
</div>
</table>
<div class="drag-handle" contenteditable="false" draggable="true" data-drag-handle="true"></div>
</div>
</div>
Here is the code:
import Table from '@tiptap/extension-table';
const TableNode = (props) => {
return (
<NodeViewWrapper className='draggable-table' >
<NodeViewContent className='content' as='table'></NodeViewContent>
<div
className='drag-handle'
contentEditable='false'
draggable='true'
data-drag-handle
/>
</NodeViewWrapper>
);
};
const DraggableTable = Table.extend({
addAttributes() {
return {
...this.parent?.(),
};
},
draggable: true,
parseHTML() {
return [{ tag: 'div[data-type="draggable-table"]' }];
},
addNodeView() {
return ReactNodeViewRenderer(TableNode);
},
});
export default DraggableTable;
Which browser was this experienced in? Are any special extensions installed?
Chrome/Firefox/Safari
How can we reproduce the bug on our side?
import Table from '@tiptap/extension-table';
const TableNode = (props) => {
return (
<NodeViewWrapper className='draggable-table' >
<NodeViewContent className='content' as='table'></NodeViewContent>
<div
className='drag-handle'
contentEditable='false'
draggable='true'
data-drag-handle
/>
</NodeViewWrapper>
);
};
const DraggableTable = Table.extend({
addAttributes() {
return {
...this.parent?.(),
};
},
draggable: true,
parseHTML() {
return [{ tag: 'div[data-type="draggable-table"]' }];
},
addNodeView() {
return ReactNodeViewRenderer(TableNode);
},
});
export default DraggableTable;
const editorConfig = {
extensions: [
StarterKit,
DraggableTable.configure({
resizable: true
}),
TableRow,
TableHeader,
TableCell,
],
};
const editor = useEditor({
extensions: [
StarterKit,
DraggableTable.configure({
resizable: true
}),
TableRow,
TableHeader,
TableCell,
],
});
Can you provide a CodeSandbox?
Not at this time, sorry. I tried but it gave me the error:
SyntaxError
No node type or group 'tableRow' found (in content expression 'tableRow+')
What did you expect to happen?
A valid table
element to be created without a div
tag in the middle of it. Perhaps for nodes like a Table
instead of inserting a div
inside the table
, it could wrap the table
.
But again, I'm, probably doing this wrong and would like any help or direction on the proper way to accomplish creating a draggable tiptap table.
Anything to add? (optional)
The only discrepancy I see between the React code and the Vue code is there is logic to handle the data-node-view-content
attribute/property in the VueNodeViewRenderer.ts module code but not the React code. I'm not sure if that would be helpful for my situation or not?
Vue: https://github.com/ueberdosis/tiptap/blob/main/packages/vue-3/src/VueNodeViewRenderer.ts#L125
get contentDOM() {
if (this.node.isLeaf) {
return null
}
const contentElement = this.dom.querySelector('[data-node-view-content]')
return contentElement || this.dom
}
React: https://github.com/ueberdosis/tiptap/blob/main/packages/react/src/ReactNodeViewRenderer.tsx#L114
get contentDOM() {
if (this.node.isLeaf) {
return null
}
return this.contentDOMElement
}
Did you update your dependencies?
- [X] Yes, I’ve updated my dependencies to use the latest version of all packages.
Are you sponsoring us?
- [ ] Yes, I’m a sponsor. 💖
I've also tried a few variants of the following but I cannot get to work either:
editor.chain().focus().insertContent('<div data-type="draggable-table"><div>YOYOYO</div></div>').run();
// editor.commands.selectTextblockEnd();
editor.chain().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
This approach has gotten me the closest but it's either removing the outer div
(<div data-type="draggable-table">
and only leaving the inner div
( as a paragraph <p>YOYOYO</p>
) or when the table is inserted, it completely removes the previous block/node.
I'm going to keep trying variants of this, but if anyone has any suggestions one way or the other, it would be greatly appreciated. TipTap is an awesome tool and we would love to fully implement/integrate this into our product line.
I recently ran into this issue as well, wanting to wrap the existing table with a custom component for extra behavior. I had to workaround this by customizing the insertTable
command of the plugin to always wrap the table node in an extra custom node. The outer TableWrapper
node is the one that uses the custom NodeView.
### Customize the Table node extension
import { default as TableExtension, createTable } from "@tiptap/extension-table";
...
export const Table = TableExtension.extend({
...
addCommands() {
return {
...this.parent?.(),
insertTable: ({ rows, cols, withHeader }) => ({ editor, commands, tr, dispatch }) => {
const node = createTable(editor.schema, rows, cols, withHeader);
if (dispatch) {
const offset = tr.selection.anchor + 1;
commands.insertContent({
type: "table-wrapper",
content: [node.toJSON()],
});
tr.scrollIntoView().setSelection(TextSelection.near(tr.doc.resolve(offset)));
}
return true;
},
deleteTable: () => ({ state, dispatch }) => {
let $pos = state.selection.$anchor;
for (let d = $pos.depth; d > 0; d--) {
let node = $pos.node(d);
if (node.type.name === "table-wrapper") {
if (dispatch) {
dispatch(
state.tr.delete($pos.before(d), $pos.after(d)).scrollIntoView(),
);
}
return true;
}
}
return false;
},
};
},
...
});
### Define your own `table-wrapper` node so TipTap knows how to render it
import React from "react";
import { mergeAttributes, Node } from "@tiptap/core";
import { ReactNodeViewRenderer, NodeViewContent, NodeViewWrapper } from "@tiptap/react";
export const TableWrapper = Node.create({
name: "table-wrapper",
group: "block",
content: "table",
parseHTML() {
return [{ tag: "table-wrapper" }];
},
renderHTML({ HTMLAttributes }) {
return ["table-wrapper", mergeAttributes(HTMLAttributes), 0];
},
addNodeView() {
return ReactNodeViewRenderer(({ editor }) => {
// Add whatever custom logic here in the table wrapper
return (
<NodeViewWrapper>
<NodeViewContent />
</NodeViewWrapper>
)
});
},
});
### Don't forget to register the new TableWrapper node
// If you are using the tiptap hook, add it to the extensions array
const editor = useEditor({
...
extensions: [...otherExtensions, TableWrapper]
...
});
This issue is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 7 days
I'm running into the same issue. Had to literally recreate the entire Table Node using the Table extension as a reference, and recreated all the DOM elements.
This issue is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 7 days
I ran into this issue as well when working to extend Table. I just want to highlight that the changes in #3984 did give me a working path forward. Currently I am using local copies of ReactNodeViewRenderer
, NodeViewContent
, NodeViewWrapper
, and useReactNodeView
with the relevant updates as a workaround-- I would love see that PR get merged!
Hey guys, did anyone manage to fix this one?