tiptap icon indicating copy to clipboard operation
tiptap copied to clipboard

Extending a Table with a NodeViewContent inserts a "div" tag where the "tbody" should be

Open githubbob42 opened this issue 2 years ago • 4 comments

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. 💖

githubbob42 avatar Jun 17 '22 01:06 githubbob42

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.

githubbob42 avatar Jun 17 '22 14:06 githubbob42

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]
    ...
});

abettke avatar Jun 24 '22 16:06 abettke

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

github-actions[bot] avatar Aug 09 '22 00:08 github-actions[bot]

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.

LyDawei avatar Aug 10 '22 23:08 LyDawei

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

github-actions[bot] avatar Dec 10 '22 00:12 github-actions[bot]

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!

theodore-faye avatar Jun 23 '23 17:06 theodore-faye

Hey guys, did anyone manage to fix this one?

gashiartim avatar Dec 04 '23 08:12 gashiartim