Readonly mode (Peer Review)

There was a question a few months ago about readonly mode. Restricting all the event listeners etc. seemed intractable. I think I have found a simple solution but would appreciate comments.

The use case is peer review. The author can edit but the peers can only comment. Of course you could restrict some actions by providing different menus, but that doesn’t restrict typing. However, because of the structure of PM, all changes to a document result in a call to Transform.step. So I created a step filter. In Transform I added

let readonly = null
export function setReadOnly(f) { readonly = f }

and in Transform.step I added

  step(step, from, to, pos, param) {
    if (typeof step == "string")
      step = new Step(step, from, to, pos, param)
    if (readonly && readonly(step)) return false // new line
    let result = step.apply(this.doc)

The filter checks the step and returns true if the step is not allowed. In Peer Review, the only menu item would be a comment button and the only steps allowed would be addMark and deleteMark for a comment mark.

This works fine in my tests. Comments?

After looking at the comment example in collab, I see that ranges are used instead of marktype. In that case the filter could just return true for the peer review use case except in the author’s PM instance.

Ranges are a better fit for annotation/comment style data than marks are (for example, if your annotated content spans blocks, or even differently marked up text, it’ll produce multiple separate marks, and it’s easy to end up with the marks being spread out over the document without any coherence).

I’m not sure about this approach to readOnly – it’d be an okay quick hack to get something working, but in the end I think we’ll have to disable contenteditable for read-only editors, so that the user agent doesn’t show interfaces that only make sense for editable content. Also, you might still want to change the content of a read-only editor through the API, and simply blocking transformations makes that impossible.

1 Like

We use marks instead of ranges for comments, for several reasons:

  • If a user copies a text range and pasts it somewhere else, and then subsequently removes the original text, we don’t want the comment to disappear. So we also connect the comment to the first mark that can be found in the text.

  • Because our backend cannot understand the document structure, it only stores an HTML-version of the document every two minutes + all the transform steps that are being send around after that. Say the HTML version of the document is at version 200, and there are 50 subsequent transforms. So a new user connecting to the document receives both and will have a document at version 250. What is a comment was added at version 225? How could we store that range on the server in a non-confusing way? We could try to map the range to either version 200 or 250, but new content may have been added or removed in a way that meant that the mapping would not necessarily be possible.

I think this depends a lot on the overall editor structure. If you want to show the text to 4 million web users who don’t have any editing rights, then yes, it’s probably better to export it and show them outside of the editor context. But if, like us, you need to have various editing access rights for different types of users (can write, can comment, can only add content but not delete, can only add tracked changes, etc.) and it is a limited amount of people accessing it that way (and they need the text to change when collaborators change things), I think it’s reasonable to use the approach of blocking certain types of transforms.

@johanneswilm That is exactly the environment we have with student peers evaluating each others prose or instructors commenting on student prose.

Although my topic was mostly about readonly, I appreciate the discussion about marks versus ranges. I wonder if a robust comment API might be something for a future roadmap. It would have to tie into existing systems such as the one described.

I have an idea for a solution to this (which would rely on pre-copy and post-paste content munging), but I haven’t implemented it yet. It seems to be a common issue, so I’ll try to make some time for it at some point.

(This is not really a setup that I am interested in supporting but) you can save ranges pretty easily though, by serializing their endpoints.

Except for the ‘add but not delete’ case, these are all perfectly compatible with a full read-only mode. And I don’t think you’ll get a very solid implementation of ‘add but no delete’ by filtering steps.

I’m okay with adding a way to block transforms (it could be added to the "beforeTransform" event), but working with these is necessarily going to be awkward, and not a great basis for a read-only feature.

All I need is the capability to restrict editing. I don’t care how it is implemented which is why I asked. I don’t know all the ramifications.

Yes, you are probably right that it will never be as “solid” as simply having contenteditable turned off if one simply wants to prevent editing. It’s for situations when it gets into more complicated areas – allow text input but no new line breaks, adding but not deletion, generally allowing editing but prohibiting the deletion of certain items, etc. that it would be good to have such an alternative. Thanks so much for considering this!

You said this before and I feel I am missing something. It seems to me that if I just have ranges, and my saving consists of saving a full version every few minutes and in-between that diffs, for ranges, I will need to save the serialized version of these ranges with every full version. For everything beyond that, I will need to record the version of the doc that the range applies to. To be sure the range was actually applied with the correct version, I will probably want the range addition to have its own version number.

Example:

My document A was last sent to the server in a full version 1:50 minutes ago at version 200. Since then 50 more steps have been applied, but we only have them in the form of diffs that were sent around. If a new client connects, that client will first take the entire document at version 200 and will additionally receive the diffs which he applies to get version 250, which is what everyone else has.

Now we add ranges to the mix. At version 200, the document already had 10 different ranges. When one of the clients sent in the full version 200 to the server, the client also sent the placement of those 10 ranges in. These ten ranges can be restores easily by adding them right after loading version 200 and before applying the other 50 steps. Three more ranges were added between 200 and 250: at 211, 231 and 241. For each of these it was recorded what number they were at the time. To add these, after loading version 200 and adding the 10 ranges, we add diffs corresponding to 11 steps, then add the first new range, then 20 more steps, then add another range, then 10 more, then the last range, then 9 more steps. Ten seconds later, another client sends in the full version of the document - at this stage with all 13 ranges and their placement at version 280 (or wherever the document is at).

This is how you are thinking?

I guess this is possible, but it requires for the server to have quite complex logic running. And I am not sure what would happen if a client doesn’t receive the addition of a specific range. If the range addition doesn’t add an extra step, then the client won’t get any notice about the missing range-addition and the diff he sends in will still be accepted…

Continued the read-only discussion in this thread