Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resizable tables don't retain their column widths when the editor isn't editable #2041

Closed
1 of 2 tasks
ConorHawk opened this issue Oct 15, 2021 · 12 comments
Closed
1 of 2 tasks
Labels
Type: Bug The issue or pullrequest is related to a bug

Comments

@ConorHawk
Copy link

What’s the bug you are facing?

I have an editable instance of the editor, which includes the table plugins. I have set the tables to be resizable. I resize my tables and save the content.

When I go to load that content into an editor that it not editable, the table columns lose the custom widths that I set.

How can we reproduce the bug on our side?

Create an instance of the editor which includes resizable tables.
Create a table and resize the columns.
Get the html from that editor editor.getHTML() and insert it as the content for another editor that is not editable.
The tables lose the custom widths that I set.

Can you provide a CodeSandbox?

https://codesandbox.io/s/unruffled-orla-fykjr?file=/src/App.vue

What did you expect to happen?

I expected the readonly version of the editor to still display the custom widths that I set, but for the columns to not be resizable.

Anything to add? (optional)

Thanks for your work on tiptap, its really awesome!

Did you update your dependencies?

  • Yes, I’ve updated my dependencies to use the latest version of all packages.

Are you sponsoring us?

  • Yes, I’m a sponsor. 💖
@ConorHawk ConorHawk added the Type: Bug The issue or pullrequest is related to a bug label Oct 15, 2021
@jakedolan
Copy link
Contributor

jakedolan commented Oct 22, 2021

Looking at this I am not entirely sure how the editor is rendering when isEditable is false, but I suspect that the editor's output is using the node's renderHTML method. The base TableCell and TableHeader do not render column widths in a way that is meaningful to a browser. But you can customize the render function to achieve this effect.

See sandbox integrated with custom TableHead (from below):
https://codesandbox.io/s/tiptap-custom-header-t1msk?file=/src/App.vue

Custom TableHeader example:

Add the style attribute and colwidth tweak (not sure it is necessary)

addAttributes() {
    return {
      ...this.parent(),
      colwidth: {
        default: null,
        parseHTML: element => {
          const colwidth = element.getAttribute('colwidth');
          const value = colwidth
            ? colwidth.split(',').map(item => parseInt(item, 10))
            : null;

          return value;
        },
      },
      style: {
        default: null,
      },
    };
  },

And renderHTML

  renderHTML({ HTMLAttributes }) {
    let totalWidth = 0;
    let fixedWidth = true;

    if (HTMLAttributes.colwidth) {
      HTMLAttributes.colwidth.forEach(col => {
        if (!col) {
          fixedWidth = false;
        } else {
          totalWidth += col;
        }
      });
    } else {
      fixedWidth = false;
    }

    if (fixedWidth && totalWidth > 0) {
      HTMLAttributes.style = `width: ${totalWidth}px;`;
    } else if (totalWidth && totalWidth > 0) {
      HTMLAttributes.style = `min-width: ${totalWidth}px`;
    } else {
      HTMLAttributes.style = null;
    }

    return [
      'th',
      mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
      0,
    ];
  },

Essentially the same needs to be done with TableCell as well.

For also ensuring the Table is aligned, you can do a custom Table like the following:

Add the style attribute

addAttributes() {
    return {
      style: {
        default: null,
      },
    };
  },

And then custom renderHTML:

  renderHTML({ node, HTMLAttributes }) {
    let totalWidth = 0;
    let fixedWidth = true;

    try {
      // use first row to determine width of table;
      const tr = node.content.content[0];
      tr.content.content.forEach(td => {
        if (td.attrs.colwidth) {
          td.attrs.colwidth.forEach(col => {
            if (!col) {
              fixedWidth = false;
              totalWidth += this.options.cellMinWidth;
            } else {
              totalWidth += col;
            }
          });
        } else {
          fixedWidth = false;
          const colspan = td.attrs.colspan ? td.attrs.colspan : 1;
          totalWidth += this.options.cellMinWidth * colspan;
        }
      });
    } catch (error) {
      fixedWidth = false;
    }

    if (fixedWidth && totalWidth > 0) {
      HTMLAttributes.style = `width: ${totalWidth}px;`;
    } else if (totalWidth && totalWidth > 0) {
      HTMLAttributes.style = `min-width: ${totalWidth}px`;
    } else {
      HTMLAttributes.style = null;
    }

    return ['div', { class: 'table-wrapper' }, ['table', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), ['tbody', 0]]];
  },

I've considered writing this up as a PR, but haven't spent enough time researching if each of the decisions in the above code are generic enough for general use. For example, I wrap each table with a <div class='table-wrapper'> that is also present in the editable editor. This ensures I can set styles like overflow-x and max-widths on tables depending on how I want them to be displayed.

@rezaffm
Copy link

rezaffm commented Oct 29, 2021

Hi, actually.... I was struggling with the issue that I would like to have the width in % and not in px. I am still learning about tiptap and your example gave me a bit of an idea how this could be solved. Basically, you can loop in the renderHTML function over all trs and their respective tds and ths and set the "style" there. Also the idea of the aligned table is brilliant.
Sorry that it doesn't really help withtthe actual issue ;-) but just wanted to express my thoughts.

@philippkuehn
Copy link
Contributor

For tables we use prosemirror-tables, which unfortunately is currently not maintained anymore. Maybe we will do a re-write ourselves someday, but this is a huge project and will take a long time. Until then, this is not supported.

@ManbokLee
Copy link

ManbokLee commented Dec 16, 2022

First of all, thank you for your contribution.
I am a user who is using the tip tab well.

Share one way to calculate the area value as a percentage in the table.
Of course, there are disadvantages.

const CustomTable = Table.extend({
    addAttributes() {
        return {
            style: {
                default: null,
            },
        };
    },
    renderHTML({ node, HTMLAttributes }) {
        let totalWidth = 0;
        let fixedWidth = true;
        let _cols = [];
        let _colSum = 0;

        try {
            const tr = node.content.content[0];
            tr.content.content.forEach(td => {
                let colwidth = 0;
                if (td.attrs.colwidth) {
                    td.attrs.colwidth.forEach(col => {
                        if (!col) {
                            fixedWidth = false;
                            totalWidth += this.options.cellMinWidth;
                        } else {
                            totalWidth += col;
                            colwidth = col;
                        }
                    });
                } else {
                    fixedWidth = false;
                    const colspan = td.attrs.colspan ? td.attrs.colspan : 1;
                    totalWidth += this.options.cellMinWidth * colspan;
                }
                _cols.push(colwidth)
                _colSum = _colSum + colwidth;
            });
        } catch (error) {
            fixedWidth = false;
        }
        const leftWidth = (totalWidth - _colSum);
        const fixedRateAverage = totalWidth / _cols.length;
        const zeroColLength = _cols.filter(x => x === 0).length;
        const fluctuationAverage = leftWidth / (zeroColLength === 0 ? 1 : zeroColLength);
        const zeroCol = Math.max(fixedRateAverage, fluctuationAverage);
        _cols = _cols.map(x => ((x === 0 ? zeroCol : x) / totalWidth * 100).toFixed());
        const noneCol = zeroColLength === _cols.length;
        const ele = noneCol ? [] : _cols.map(x => ['col', mergeAttributes(this.options.HTMLAttributes, { style: `width: ${x}%;` })]);

        if (noneCol) {
            HTMLAttributes.style = null;
        } else  if (fixedWidth && totalWidth > 0) {
            HTMLAttributes.style = `width: ${totalWidth}px; max-width: 100%;`;
        } else if (totalWidth && totalWidth > 0) {
            HTMLAttributes.style = `width: ${totalWidth}px; max-width: 100%;`;
        } else {
            HTMLAttributes.style = null;
        }

        return [
            'div', 
            { class: 'table-wrapper' }, 
            [
                'table', 
                mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 
                ['tbody', 0],
                ['colgroup', {}, ...ele]
            ]
        ];
    },
});

If we complement the function at this point,
When adjusting the width of the column, I have to create the width of all the columns, but I don't know how.

@ollejernstrom
Copy link

ollejernstrom commented May 9, 2023

Just let editable be true in the editor and set attributes border, pointerEvents, contenteditable and tabindex to make it unaccessable. At least it worked for me.

@rezaffm
Copy link

rezaffm commented Jun 4, 2023

If you have something like this ...

          CustomTable.configure({
            cellMinWidth: 100,
            resizable: true
          }),

the result is not as expected as the table will be not wrapped in a div on insert.

@CapitaineToinon
Copy link

Why is this issue closed? Why are the tables not retaining their styles when saves to HTML? I don't get it, really seems like a bug to me. Makes the resiable feature completely useless

@rezaffm
Copy link

rezaffm commented Jun 28, 2023

#4105

@sjdemartini
Copy link
Contributor

I did the following to get this to work in my package mui-tiptap (https://github.com/sjdemartini/mui-tiptap), which has a TableImproved extension:

  1. Extend the Table extension so that the columnResizing plugin is always included if the resizable option is true, even if editable is false. This ensures that whether the editor starts in editable mode or in read-only mode, the plugin will be registered (since changing editable doesn't re-run plugin registration, so we can't include editable as a part of the condition like the original Table extension does). By doing so, the column widths will always be respected/retained, and switching editable state will not affect that. A condensed version of the mui-tiptap code:
    const TableImproved = Table.extend({
      addProseMirrorPlugins() {
        return [
          ...(this.options.resizable
            ? [
                columnResizing({
                  handleWidth: this.options.handleWidth,
                  cellMinWidth: this.options.cellMinWidth,
                  View: this.options.View,
                  lastColumnResizable: this.options.lastColumnResizable,
                }),
              ]
            : []),
    
          tableEditing({
            allowTableNodeSelection: this.options.allowTableNodeSelection,
          }),
        ];
      },
    });
  2. Update the CSS such that column resizing is not possible in read-only mode (despite the plugin being registered). This is what @ollejernstrom suggested above Resizable tables don't retain their column widths when the editor isn't editable #2041 (comment). Here are the important styles from the mui-tiptap code (written in tss-react syntax):
    // Only when the editor has `editable` set to `true` should the table column resize tools should be revealed and usable
    '.ProseMirror[contenteditable="true"]': {
      "& .column-resize-handle": {
        position: "absolute",
        right: -2,
        top: -1,
        bottom: -2,
        width: 4,
        zIndex: 1, // ensure the handle sits above the cell backgrounds
        backgroundColor: "gray",
        pointerEvents: "none",
      },
      "&.resize-cursor": {
        cursor: "col-resize",
      },
    },
    
    '.ProseMirror[contenteditable="false"]': {
      "& .column-resize-handle": {
        display: "none",
      },
    
      "&.resize-cursor": {
        // To ensure that users cannot resize tables when the editor is supposed to be read-only, 
        // we have to disable pointer events for the entire editor whenever the resize-cursor
        // class is added (i.e. when a user hovers over a column border that would otherwise 
        // allow for dragging and resizing when in editable mode). This is because the underlying
        // prosemirror-tables `columnResizing` plugin doesn't know/care about `editable` state, 
        // and so adds the "resize-cursor" class and tries to listen for events regardless.
        pointerEvents: "none",
      },
    },

Hope that may be helpful to some folks! I would contribute this upstream to tiptap, but I assume since CSS isn't bundled in general, it's not desirable to have the resizable column JS behavior for the editor on by default, requiring custom CSS to prevent resizing when editable is false. (It still feels a bit hacky, working around the plugin quirks.)

If you install mui-tiptap, it bundles the default styles so it should "just work" when you use TableImproved. There are a number of other extensions (HeadingWithAnchor, FontSize, ResizableImage) and components (LinkBubbleMenu, TableBubbleMenu, an all-in-one RichTextEditor component, a bunch of menu buttons & selects for the various extensions, etc.) in mui-tiptap too, in case any of that is of interest to folks.

@m3di
Copy link

m3di commented Oct 20, 2023

i used this as a quick fix:

TableCell.extend({
    renderHTML({ HTMLAttributes }) {
        const attrs = mergeAttributes(this.options.HTMLAttributes, HTMLAttributes);

        if (attrs.colwidth) {
            attrs.style = `width: ${attrs.colwidth}px`;
        }

        return ['td', attrs, 0];
    }
})

@ryanb
Copy link

ryanb commented Dec 18, 2023

@philippkuehn Is there a reason the "quick fix" above can't be part of TableCell? It seams reasonable that if there's a colwidth attribute it would set the width in the resulting HTML. Would it be possible to reopen this issue?

@splincode
Copy link

@m3di thank you! it's really working and also table { table-layout: unset; }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Type: Bug The issue or pullrequest is related to a bug
Projects
None yet
Development

No branches or pull requests