quill icon indicating copy to clipboard operation
quill copied to clipboard

Add support for tables

Open jhchen opened this issue 11 years ago • 136 comments

As an administrative note, I will delete all non-substantive comments from this thread to cut down the clutter and mass emails to everyone. Please use the Reaction feature to show support.

jhchen avatar May 22 '14 23:05 jhchen

Right now I'm taking Quill as the first option to use in a big education related software, but the support for tables is a "deal breaker", is there any progress in the implementation of this feature?

arturocr avatar Jul 31 '14 20:07 arturocr

From some preliminary research, tables are going to be complex to implement and probably won't be added in the immediate future.

jhchen avatar Aug 01 '14 19:08 jhchen

@jhchen thanks for the answer, I understand. Thanks anyway, QuillJS looks really promising, will keep an eye on it and see if I can contribute with something.

arturocr avatar Aug 01 '14 20:08 arturocr

Table editor in closure library for reference https://rawgit.com/google/closure-library/master/closure/goog/demos/editor/tableeditor.html

benbro avatar Feb 07 '15 13:02 benbro

I would need that for my project too. I think I can spend some time on this, if someone can advice me some API endpoints or something. Where should I begin with ?

cubitouch avatar Mar 11 '15 09:03 cubitouch

quill supports tabs and automatically expand and shrink the width of each block when adding/removing text. Maybe we can define columns with tabs and add the table borders as a layer ontop of the text. The column width in the table can be synced with the width of the text in each block separated by tabs. Column width in all rows should be synced. Maybe a custom element can be inserted with a configurable width. (similar to how images are inserted).

First Header (tab) Second Header (tab)
Content from cell 1 (tab) Content from cell 2 (tab)
Content in the first column (tab) Content in the second column (tab)

A simple table GUI to add/remove rows and columns might be enough: https://wymeditor.github.io/wymeditor/dist/examples/21-table-plugin.html

benbro avatar Sep 02 '15 12:09 benbro

would it be possible to allow pasting of tables to not convert them to divs? tried to add the tags to the normalizer whitelist, but it still converted them. I don't mind working with a tweaked core lib until 1.0 is out. Any kind of direction would be greatly appreciated!

ghost avatar Feb 25 '16 18:02 ghost

I really want to take this on but I have some questions. The first one being that I'm really curious what thoughts @jhchen and @george-norris-salesforce have about how to go about doing this. The way I see it there are three levels of support: 1. Handling pasted tables. 2. Providing APIs for interacting with tables 3. Adding UI support for tables

  1. Creating blots in parchment for <table> ,<thead>, <tbody>, <tr> and <th>
  2. Adding a tables module that brings in these blots
  3. Possibly updating clipboard.js to handle tables properly, I think the blots will make this work, but I'm not that familiar with the code.
  4. Chasing down editor bugs when interacting with tables. Content-editable does...things with tables that I don't understand

The next level is the hard part!

  1. Define APIs required (insert/delete col, row, do things with header, etc)
  2. Write test suite
  3. Implement APIs

And finally

  1. Decide proper UI for managing tables
  2. Build UI and wire up with API

Oh and one last thing: how about a Quill slack team?

saw avatar Sep 02 '16 17:09 saw

Adding tables would be a great addition to Quill, both in beneficial impact but also work required.

I think the key to success here is to start small. So I would scope the initial feature set to:

  • Tables are made of NxM cells, no cell taking up more than one row or column
  • Tables cannot be resized after creation
  • No special header or footer rows or columns

The first major decision is whether tables should be defined/implemented with multiple Blots (like how lists are implemented) or a single Embed Blot (like formulas or videos). The latter is more straightforward but also more constraining but as a starting point of the discussion we'll start here.

For reference, an expected table would look like this:

<table>
  <tbody>
    <tr>  
      <td>A1</td>
      <td>B1</td>
      <td>C1</td>
    </tr>
    <tr>
      <td>A2</td>
      <td>B2</td>
      <td>C2</td>
    </tr>
  </tbody>
</table>

Inserting such a table could look like this:

insertEmbed(index, 'table', [
  ['A1', 'B1', 'C1'],
  ['A2', 'B2', 'C2']
]);

Deleting the table can use the existing deleteText with appropriate index.

Embeds currently have the limitation that they cannot be updated--one would delete and insert a new table to update. This also more so affects large tables and realtime collaboration use cases. Some sort of update semantic for embeds could probably be introduced to Deltas and tables can just piggyback off of that. The only ramification I can think of is it may standardize around using objects so our definition might change to:

insertEmbed(index, 'table', {
  '1': {
    '1': 'A1', '2': 'B1', '3': 'C1'
  },
  '2': {
    '1': 'A2', '2': 'B2', '3': 'C2'
  }
});

// Theoretical update embed
updateEmbed(index, {
  '1': {
    '3': 'Updated C1 value'
  }
});

The keys dont neccessarily have to be indexes and can be used to bear more meaning, for example to indicate header status.

The other major limitation to implementing as an Embed Blot is: it is not clear how to support formatting within cells. I personally don't think it's a good idea to nest complex formats inside a small table cell anyways but simple formats like bolding within cells seem reasonable. Perhaps this could be an acceptable limitation of an inital table implementation however.

Implementing tables as multple blots would have neither of Embed Blot's limitations, but introduces more decision points to clear up ambiguity it introduces:

  • Which of table, row and/or cell a Block Blot?
  • How will tables coexist with other Block level formats like lists, headers, etc?
  • How will a table's Delta representation look like?

There are a lot of paths here and I would encourage others interested to try and prototype one of these routes and report any unexpected issues or unexpected lack of issues.

Once a table is defined with Parchment, pasting should just work since initialization actually uses the clipboard.convert function. The only additional effort is if Quill's tables support a subset of HTML table feature (which will be the case) and you'd like to handle this specially. For example, if the above Embed Blot implementation route is taken without special treatment of <thead> Quill will just clump that with regular <tbody> rows, but if you'd rather strip <thead> content for some reason, you would implement a custom clipboard matcher.

To be clear tables need to coexist within the Delta format and semantic. Otherwise general and important methods like getContents, updateContents, setContents would break. With this satisfied, I don't see a need for top level table specific APIs. No other format requires this and it's not clear why tables would be special.

As far as UI I think doing what existing products like Word/Docs does is a good starting point. There are unused icons in quill/assets/icons/ that can be helpful for tables. Implementing good UIs is non-trivial work but does not introduce any implementation design risk I can think of.

Let's see how far discussion in this thread can get us.

jhchen avatar Sep 03 '16 23:09 jhchen

As an administrative note, I will delete all non-substantive comments from this thread to cut down the clutter and mass emails to everyone. Please use the Reaction feature to show support.

jhchen avatar Sep 03 '16 23:09 jhchen

Embeds seem like the simplest solution, I like the approach to creating a table @jhchen laid out. One problem with this is pasted tables, which often come with a lot of formatting. We can assume people are using a reset style sheet that applies border-collapse:collapse and border-spacing:0; but all the attributes in the pasted table will need to then be stripped.

In the use cases I'm dealing with right now people really, really want to past tables from Word and Excel, which come with backgrounds and borders in inline styles.

Is there a way to strip these attributes without creating blots for td and td?

saw avatar Sep 05 '16 19:09 saw

I've had some time to work on this today and I bumped into the issues @jhchen alluded to regarding formatting. Formatting cells with background colors as well as having contents with inline styles seems important. The trouble is making this work without a parallel api. Heres one way a table API with formatting could look:

insertEmbed(index,'table',[
  [{
    content:'Hello',
    style: {
      backgroundColor: 'red',
      borderTop: '1px solid black',
      color: 'white',
      weight: 'bold'
    }
  },'World'],
  ['A', 'B']
]);

Resulting in:

<table>
  <tbody>
    <tr>
      <td style="background:red;border-top:1px solid black;color:white;"><b>Hello</b></td>
      <td>World</td>
    </tr>
    <tr>
      <td>A</td>
      <td>B</td>
    </tr>
  </tbody>
</table>

Or are we getting to far into a world of parallel apis? Thoughts on this approach?

saw avatar Sep 11 '16 05:09 saw

I'm trying to get some real data on our users that are currently pasting tables into CK Editor from word so I get a sense on what the "normal" interaction for pasted tables is.

saw avatar Sep 11 '16 05:09 saw

I think a sensible "limited" table to emulate is tables in github markdown:

  • Cells cannot contain block elements
  • Cells cannot contain tables
  • No colspan (maybe?)
  • Tables will not directly editable except for editing the text content (copying off this thread for draft.js)

saw avatar Sep 11 '16 17:09 saw

Hi, jhchen! I would like to know when planning add support table. Thanks

kolesoffac avatar Oct 05 '16 15:10 kolesoffac

I posted a proof-of-concept Gist based on @saw's example, extending it to work with a React component that builds the table structure given some data. It works okay, but it's far from a complete implementation.

ccashwell avatar Oct 06 '16 19:10 ccashwell

Yeah, it works ok, it has some limitations. I do want to finish this PR as soon as I can, I've built more extensive table support for work, but our use case right now is just pasting tables, not actually creating or editing them.

On Thu, Oct 6, 2016 at 12:07 PM, Chris Cashwell [email protected] wrote:

I posted a Gist https://gist.github.com/ccashwell/e2ceac25d7195e4715a93587da89d747 based on @saw https://github.com/saw's example, extending it to work with a React component that builds the table structure given some data. It works okay.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/quilljs/quill/issues/117#issuecomment-252059016, or mute the thread https://github.com/notifications/unsubscribe-auth/AABT5zr5uJpkgA0rIKH1oUoSJF96BCYBks5qxUbUgaJpZM4B9jc3 .

saw avatar Oct 06 '16 19:10 saw

Would like to see this pretty please :) :+1:

mscreenie avatar Nov 11 '16 15:11 mscreenie

I've also been working on adding tables to Quill and I think it's going quite well at the moment. I'm using Quill as the heart of a collaborative editing experience in our app - multiple users editing the same stuff, like Google Docs. Quill has been awesome and has made this part of the application really clean.

So onto my tables. I did not like the embed approach, as it meant tables were really just a blob in the document, and editing the contents of a table cell was super limited with that approach.

Instead, I have approached the problem by treating tables in a similar was to lists in Quill. A ListItem is a node that lives inside a List and Quill handles this structured content well, creating the outer most <UL> when needed and adding <LI> elements inside it. I figured the same approach would work for tables. They are basically the same, but with a few extra layers 👍

So, I have a Table, a TableRow and a TableCell that all wrap the things below them.

The other benefit is that table cells can now contain any block level elements, like headings, lists, paragraphs, etc. All inline formatting works as well. They also work seamlessly with my collaborative editing features.

I'd say it has been a total nightmare getting the structure to remain sound, mostly due to my poor understanding of the internals of Parchment, but it is possible to add cells and rows and have everything correctly merged into place.

There is still a ton to do, but if anyone else has taken this approach, I'd be interested in talking.

Here is a quick screenshot of Quill, with tables, and someone else editing them...

gathercontent_editor_prototype

rikh42 avatar Jan 02 '17 12:01 rikh42

Nice work @rikh42 ! We've been using the tables I created in production but I haven't had time to build the quill ui for it (we do not use the quill buttons but our own custom ui). The embed approach works, but to add features beyond "just tables" requires making more or less an entire parallel UI for tables. I spent a day or two going down the path you have but it was very complex and didn't seem realistic given our time constraints. Do you have your table format in a fork?

saw avatar Jan 02 '17 16:01 saw

@saw I certainly learnt a lot by going over your PR a few times, so thanks for pioneering that!

It's in a branch of our internal project at the moment. It has turned out to be pretty complex, and I am sure there are still a few edge cases to figure out. (Well, there isn't a lot of code to get it working, but figuring out what the code should be, that was tricky :-)

I'm just working on all the keyboard handlers and UI components we'll need at the moment. My goal is to make something comparable to Tables in Google Docs.

rikh42 avatar Jan 02 '17 16:01 rikh42

Nice work @rikh42 ! What does a Delta look like for a document with a table?

jhchen avatar Jan 02 '17 22:01 jhchen

Here is an extract from the middle of a larger document showing the first 3 columns in a table (JSON)...

There is some additional styling in there that you can ignore. who is a kind of authorship feature, but the bit you'll want to look at is the table-cell property. You'll see that they all have the same table and row id's as everything here belongs in a single table row in the same table. Additional blocks would just have the same cellId, and everything is resolved during insertion. No tbody or th at the moment, but I can live with that for now.

{
	"insert": "Description",
	"attributes": {
		"who": 28167,
		"bold": true
	}
}, {
	"insert": "\n",
	"attributes": {
		"who": 47194,
		"align": "center",
		"table-cell": {
			"cellId": "table-cell-zxwscqtywssf",
			"rowId": "table-row-oksghsnmwiwc",
			"id": "table-id-tpbprwzxdlvn"
		}
	}
}, {
	"insert": "Owner",
	"attributes": {
		"who": 28167
	}
}, {
	"insert": "\n",
	"attributes": {
		"who": 28167,
		"table-cell": {
			"cellId": "table-cell-epkzpvnrkoeu",
			"rowId": "table-row-oksghsnmwiwc",
			"id": "table-id-tpbprwzxdlvn"
		}
	}
}, {
	"insert": "Cell 3",
	"attributes": {
		"who": 6939
	}
}, {
	"insert": "\n",
	"attributes": {
		"who": 28167,
		"table-cell": {
			"cellId": "table-cell-aerxfqxowhfd",
			"rowId": "table-row-oksghsnmwiwc",
			"id": "table-id-tpbprwzxdlvn"
		}
	}
}

Which results in the following HTML in the editor...

<table data-wrap-id="table-id-tpbprwzxdlvn">
	<tr data-wrap-id="table-row-oksghsnmwiwc">
		<td data-wrap-id="table-cell-zxwscqtywssf">
			<p class="ql-align-center"><strong>Description</strong></p>
		</td>

		<td data-wrap-id="table-cell-epkzpvnrkoeu">
			<p>Owner</p>
		</td>

		<td data-wrap-id="table-cell-aerxfqxowhfd">
			<p>Cell 3</p>
		</td>
	</tr>
</table>

rikh42 avatar Jan 03 '17 08:01 rikh42

It would be nice to have a simple implementation of tables in the Quill. Really. I don't ask to give me something hard and serious — only basic support.

monolithed avatar Jan 29 '17 15:01 monolithed

@rikh42 impressive work on table support! 🚀 Looks like there are many more that would like to play around with this, is it perhaps something or at least parts of it you could extract and share with the community?

MrTin avatar Jan 29 '17 21:01 MrTin

I think I have an idea of how @rikh42 implemented it. I tried to hack my implementation based on the list and list-item blots. Lets say we are implementing 3 new blots: table, table-row, table-cell. The main differences from the way the list is implemented are:

  1. Instead of working with format on the level of the outermost container (in lists the ul and ol are responsible for the format, not the li), we have to work with the format on the level of the inner-most container (table-cell).
  2. When we create a list -- we create the outermost container. For example, when you initially render 10 list items, quill actually renders each list item in a separate ul, and then it creates defaultChild item within each ul. Then the optimize() function fires for each ul and it ends up moving all li items into the first ul. But since in the tables we create the inner-most container (table-cell), we need its optimize() to also do some work by first wrapping itself into table-row. Then table-row's optimize() needs to wrap itself into table.
  3. And that's not it! If we want to have paragraphs and lists inside table-cell then the table-cell should also be a Container and we need to patch Block and probably List to be aware of the table logic and to be able to wrap itself into table-cell (which then wraps itself into table-row, which then wraps itself into table)

Well, that's what my current thoughts are, I don't actually have a working solution yet, so it would be really nice to hear from @rikh42 or anyone else hacking this feature.

cray0000 avatar Jan 30 '17 14:01 cray0000

I agree @rikh42 has the right idea, but it's very tricky, I would love to see their code to understand how they made it happen On Mon, Jan 30, 2017 at 6:20 AM Pavel Zhukov [email protected] wrote:

I think I have an idea of how @rikh42 https://github.com/rikh42 implemented it. I tried to hack my implementation based on the list and list-item blots. Lets say we are implementing 3 new blots: table, table-row, table-cell. The main differences from the way the list is implemented are:

  1. Instead of working with format on the level of the outermost container (in lists the ul and ol are responsible for the format, not the li), we have to work with the format on the level of the inner-most container (table-cell).
  2. When we create a list -- we create the outermost container. For example, when you initially render 10 list items, quill actually renders each list item in a separate ul, and then it creates defaultChild item within each ul. Then the optimize() function fires for each ul and it ends up moving all li items into the first ul. But since in the tables we create the inner-most container (table-cell), we need its optimize() to also do some work by first wrapping itself into table-row. Then table-row's optimize() needs to wrap itself into table.
  3. And that's not it! If we want to have paragraphs and lists inside table-cell then the table-cell should also be a Container and we need to patch Block and probably List to be aware of the table logic and to be able to wrap itself into table-cell (which then wraps itself into table-row, which then wraps itself into table)

Well, that's what my current thoughts are, I don't actually have a working solution yet, so it would be really nice to hear from @rikh42 https://github.com/rikh42 or anyone else hacking this feature.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/quilljs/quill/issues/117#issuecomment-276073767, or mute the thread https://github.com/notifications/unsubscribe-auth/AABT5x9FUOm1XeuFJxwGbTotm6kOwH_dks5rXfG7gaJpZM4B9jc3 .

saw avatar Jan 30 '17 16:01 saw

@cray0000 in that approach how do you determine what <td> belong to which <tr>? It seems if you work up from a the cell you have no way to determine when to wrap in <tr> that matches the defined table during the value() phase, unless I am not seeing it.

saw avatar Jan 30 '17 18:01 saw

@saw,

table-cell will have ids for <tr> and for <table>. And if we want to have lists and paragraphs within td then we also need the id for td too). But for now lets take the simple case of 2 ids -- row and table

Actually the tableId is optional, depends on whether we want 2 separate tables to automerge together or not. For example list does automatically merge together with the list next to it. If you have 2 of them separated by <p> and you remove that <p> -- then those 2 lists will be merged into one. The same is true for the table implementation. If we take into consideration tableId, then 2 separate tables can exist next to each other. Otherwise they will be always merged into a single table (even though the amount of columns may not be equal).

So lets say we want both rowId and tableId and we don't want <p>/<li> within <td>. Here is my suggested algorithm (I didn't actually test it, it's just an idea):

table-cell

  1. we create table-cell with the value of { rowId: 'aaa', tableId: 'bbb' }
  2. we specify the format treatment in static create() and static formats() to attributes data-table-id, data-row-id.
  3. in optimize() we check whether the parent is table-row. If not -- we wrap it into table-row by creating that blot and passing it the same value -- Parchment.create('table-row', this.statics.formats(this.domNode))

table-row

Here is where the merging of <tr>s is going to happen (moving <td> into a single <tr> they belong to).

  1. table-row will be created by its child (table-cell) with the same value. static create() and static formats() will set data-table-id, data-row-id

  2. optimize() will do 2 things: a) merge sibling table-rows together by data-row-id -- this is done the same way as currently in list:

    if (next != null && next.prev === this &&
      next.statics.blotName === this.statics.blotName &&
      next.domNode.getAttribute('data-row-id') === this.domNode.getAttribute('data-row-id')
    ) {
      // move stuff from one <tr> to another, the same as in 'list':
      next.moveChildren(this);
      next.remove();
    }
    

    b) check whether the parent is table. If not -- we wrap it into table while passing the same value to Parchment.create.

table

The table blot is basically the same as table-row but it's not interested in rowId anymore and only cares about the tableId and the corresponding attribute data-table-id. And it has the same optimize() as table-row which merges tables together based on the data-table-id attribute, but since the table blot is the highest level we don't need to wrap it into anything.

cray0000 avatar Jan 30 '17 18:01 cray0000

I think <thead><th></th>...</thead> will need some love in all this. Some people will want to use this sort of thing against GFM.

wells avatar Feb 02 '17 17:02 wells