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

Already on GitHub? Sign in to your account

Rich text support #1

Open
josephg opened this Issue May 3, 2011 · 95 comments

Comments

Projects
None yet
Owner

josephg commented May 3, 2011

ShareJS needs a set of OT functions for editing rich text. They should support annotations (bold, italics, etc) of some sort and there should be a plan on how to embed objects like images in documents.

amark commented Nov 30, 2011

I have already been working on this, been developing a jquery extension for tracking caret position differences in updated contenteditable containers. However it has been a while, and I never finished it - I'll look at it again, and try and comment an update here - hopefully it would be able to work. No attempt for images, however.

Owner

josephg commented Nov 30, 2011

Cool :)

amark commented Feb 13, 2012

Hey, sorry, I worked on this for a little bit but didn't finish it (got stuck) and then went back to working on other things for the last few months. Computer crashed, lost the old work I had done, so I had to start afresh. I was checking out your Diff algorithm in textarea.js, and I noticed there is a bug if it is used on strings of HTML:

To simplify, take the function and make it have a callback so we can play with it in the console.

applyChange = function(doc, oldval, newval) {
    var commonEnd, commonStart;
    if (oldval === newval) return;
    commonStart = 0;
    while (oldval.charAt(commonStart) === newval.charAt(commonStart)) {
      commonStart++;
    }
    commonEnd = 0;
    while (oldval.charAt(oldval.length - 1 - commonEnd) === newval.charAt(newval.length - 1 - commonEnd) && commonEnd + commonStart < oldval.length && commonEnd + commonStart < newval.length) {
      commonEnd++;
    }
    if (oldval.length !== commonStart + commonEnd) {
      doc(commonStart, oldval.length - commonStart - commonEnd);
    }
    if (newval.length !== commonStart + commonEnd) {
      return doc(commonStart, newval.slice(commonStart, (newval.length - commonEnd)));
    }
  };

All that did was erase ".del" and ".insert", so we can now do:

applyChange(function(start,length){
console.log(start);
console.log(length);
},"hello world","hello");
// 5
// 6

So now we can see our diff:

var text = "hello world";
var textOld = "hello"
applyChange(function(start,length){
console.log(start);
console.log(length);
console.log(text.slice(start,start+length));
},text,textOld);
// 5
// 6
// " world"

Okay, good it works.
But, it FAILS on HTML, under specific conditions:

var text = "Hello <b>world <i>oops!</i></b>";
var textOld = "Hello <b>world </b>"
applyChange(function(start,length){
console.log(start);
console.log(length);
console.log(text.slice(start,start+length));
},text,textOld);
// 16
// 12
// "i>oops!</i><"
// should be: "<i>oops!</i>"

I know that the Diff algo is suppose to work on text, so I shouldn't assume that you intended it to work on HTML strings - therefore I'm not posting this as an issue, but you should definitely be aware that anybody trying to use ShareJS to transfer HTML string differences will get mismatching deltas.

Anyways, I just wanted to post this now, before trying to fix it - I'm sure it isn't that hard, I'm going to go attempt to do that right now... but I'm a super slow coder :P, so I just wanted to alert you first in case I forget.

Also, I originally was working on rich text with ShareJS but now I'm working on it with my own setup, I calculate the offset of the caret with respect to the HTML nodes from both left and right directions, which requires transferring a lot more information than a single offset inside of a string. I as having difficulty rebuilding the caret position into the correct node based solely off of a string offset (which is what I was trying to do before), so I gave it up. I'm sure it is possible, and is probably a lot more efficient than what I'm doing... but I'm having better success with my method, so I'm not sure how compatible it will be with ShareJS. But I'll definitely try. :)

Thanks!

Owner

josephg commented Feb 14, 2012

Thanks for letting me know, but that algorithm isn't meant to be used for rich text, or html editing. (Unless you're using sharejs to edit the html as text, in which case it won't really matter.) Its not even a proper diff algorithm - its just a hack to get around html's awful & inconsistant events.

Also, please make new issues for bug reports instead of tacking them on other issues.

Hey Joseph,

I've been thinking about a way to support rich text, but it would be a pretty drastic departure from traditional OT, and I'm not even entirely sure it's possible given browser capabilities. I figured given your very vocal position on wanting unbloated/complicated code as much as possible that I'd at least present the idea.

What I'm thinking is something along the lines of the way real time video games work (a subset of the dead reckoning algo) basically the central server maintains a clock which is sync'd to clients over a socket protocol. The clients would submit operations to the server, providing operations and an associated timestamp. The server would receive these, broadcast the client change to all clients, and locally apply updates to the doc in order. Both the server and client would maintain a buffer of previous states, so that they could roll back, apply changes, and then roll forward. Both client and server would run this same algorithm, but the server's instance would be the authoratative copy that gets persisted.

The biggest challenge I can foresee to this right now is it would require the clients to have some way to identify "i'm changing this particular DOM node or nodes, and heres how to get to it in the document". kind of a 2 fold problem:

  • mutation support is limited in most browsers - maybe this could be circumvented by picking up key and mouse events on the document and maintaining state that way
  • not sure how to efficiently identify a series of DOM nodes in a document in such a way that it could be sent to other clients. maybe one solution is to have some kind of move list "start at the top node, go this sibling, go down another node, go to 4th sibling, etc"

Anyway, I know this is kind of a half baked idea but i think if the dom change identification problem could be solved this might be feasible, and probably has applications beyond sharejs.

Would love to hear any thoughts you have, and thanks for the awesomeness that is sharejs. : )

amark commented Feb 26, 2012

I am implementing this, without a master server. Already written all the Range code for it, and a bunch of stuff. Probably several more weeks of work. But it works nicely. If Joseph doesn't care for it, @mreinstein give me a message and we can talk more separately.

@amark,

How are you doing it without a master server? Browsers dont have any peer to peer mechanism that I'm aware of. You'd need to distribute changes from one client to all the others, so I assume there needs to be a server to act as the "reflector" to get these between browsers. I have a pretty good handle on the state management piece, its mostly identifying and sending the dom changes that has me a bit stumped. Would love to hear more!

amark commented Feb 26, 2012

Of course I have a relay server, but it doesn't act as the ultimate authority -- just like another client. Please message me (it won't allow me to message you) separately, I do not want to bombard Joseph with notifications or go offtopic for this issue.

bergie commented Mar 14, 2012

Rich text support in ShareJS would be great for Create. Let me know if I can help (testing, experimenting) somehow. We have an issue tracking this.

Owner

josephg commented May 5, 2012

Cheers guys. As you predicted, I don't want to add a different concurrency algorithm to sharejs. Its possible to do rich text using ShareJS's OT implementation, its just going to be a bit of work. Best of luck getting your thing working though, that sounds neat.

dlee commented Jul 20, 2012

@josephg Any updates on a rich text editor using ShareJS's OT implementation?

Owner

josephg commented Jul 31, 2012

I haven't written any code for rich text. Patches welcome!

Contributor

wmertens commented Aug 7, 2012

Does #112 have everything this issue wants?

Ok I think I have something pretty useful but its kind of buggy at the moment.

I am using an iframe to do my text editing and sharejs is listening to that document.
Setting text needs to be used with .innerHTML but when I call this function it resets all the ranges in the document to the start.

The replaceText function looks like:

   // Text replacer, important to move the cursors when you pull a .html()
replaceText = function(newText) {
    var anchor, focus, scrollTop, _ref, _ref1, _ref2;
    scrollTop = elem.scrollTop;

    var caret = getCaret();

    if (elem.scrollTop !== scrollTop) {
        elem.scrollTop = scrollTop;
    }

    elem.innerHTML = newText;

    return setCaret(caret.selectionStart, caret.selectionEnd);
}

   // And the Caret get/set functions
getCaret = function() {
    var iframe = $('.wysihtml5-sandbox')[0],
        caret  = iframe.contentWindow.getSelection();

    return {
        selectionStart     : caret.baseOffset,
        selectionEnd       : caret.extentOffset,
        isCollapsed        : caret.isCollapsed,
        caretPosition      : caret.focusOffset,
        selectionDirection : (caret.baseOffset < caret.extentOffset) ? 'forward' : 'backward',
        original: function() {
            return caret;
        },
        clone: caret,
    }
}

   setCaret = function(start, end) {
    var iframe = $('.wysihtml5-sandbox')[0],
        el = iframe.contentDocument.body;

    var range = document.createRange(),
        sel   = window.getSelection();

    var startContainer = range.startContainer;
    var endContainer = range.endContainer;

    range.setStart(startContainer, start);

    if (end) {
        range.setEnd(endContainer, end);
    }
    else {
        range.collapse(true);
    }

    sel.removeAllRanges();
    sel.addRange(range);
    el.focus();
}

Using this we can determine the line number (dom node) and the cursor position from the start.

Thoughts? It seems to be working great for me. Sometimes multi-line edits fail and merge together but sharejs does most of the patching right.

ajb commented Jan 24, 2013

@scrummitch i imagine that would run into issues when one user is typing and another user is also typing above her in the document. when user #2 inserts a character, user #1's cursor shifts from its current position. does that make sense?

mb21 commented Jan 29, 2013

Have you guys read this Stackoverflow discussion? I got the impression that you cannot do HTML editing with traditional OT but have to either use a different markup notation (like Google Wave did) or do something like in this paper by Davis et al. Which approach do you think is more promising for sharejs?

Contributor

wmertens commented Jan 29, 2013

On Jan 29, 2013, at 13:14 , mb21 notifications@github.com wrote:

Have you guys read this Stackoverflow discussion? I got the impression that you cannot do HTML editing with traditional OT but have to either use a different markup notation (like Google Wave did) or do something like in this paper by Davis et al. Which approach do you think is more promising for sharejs?

There are trade-offs on both sides.

Treating the HTML document as a DOM object that you manipulate with JSON OT is a very natural mapping since you already have the DOM available. However, this makes text operations like searching etc cumbersome.

Storing attributes as ranges fixes that but converting to/from HTML becomes a slower operation.

Personally I prefer the DOM mapped object approach.

Wout.

ajb commented Jan 29, 2013

My naive opinion:

Trying to use a different markup notation seems like a pain.

contenteditable is fantastic, and anything that's supported in contenteditable needs to be able to be represented in a ShareJS document.

Etherpad approaches this by assigning a unique id attribute to each node in the DOM. Is this a feasible route for ShareJS?

Contributor

wmertens commented Jan 29, 2013

Isn't this something that should be solved by the editor? All browsers make
different HTML for the same view and the editor has to sanitize that with a
simplified DOM copy.

I believe http://aloha-editor.org/features.php does that, perhaps others as
well.

On Tue, Jan 29, 2013 at 7:14 PM, Adam Becker notifications@github.comwrote:

My naive opinion:

Trying to use a different markup notation seems like a pain.

contenteditable is fantastic, and anything that's supported in
contenteditable needs to be able to be represented in a ShareJS document.

Etherpad approaches this by assigning a unique id attribute to each node
in the DOM. Is this a feasible route for ShareJS?


Reply to this email directly or view it on GitHubhttps://github.com/josephg/ShareJS/issues/1#issuecomment-12848938.

Contributor

nornagon commented Jan 29, 2013

One major problem with this approach is the need to transform these
operations by each other:

document: {text: "this is some text"}
op A: [{p:["text",8],sd:"some"}] --> {text: "this is text"}
op B: [{p:["text"],od:"this is some text",oi:["this ",{b:"is"}," some
text"]}] --> {text:["this ",{b:"is"}," some text"]}
intended final document: {text: ["this ",{b:"is"}," text"]}
actual final document, with current JSON OT: {text:["this ",{b:"is"}," some
text"]}

i.e., op A is lost.

the JSON OT implementation doesn't have any way to split text and maintain
intention.

Rich text definitely needs a dedicated OT type.

j

On Wed, Jan 30, 2013 at 5:48 AM, wmertens notifications@github.com wrote:

Isn't this something that should be solved by the editor? All browsers
make
different HTML for the same view and the editor has to sanitize that with
a
simplified DOM copy.

I believe http://aloha-editor.org/features.php does that, perhaps others
as
well.

On Tue, Jan 29, 2013 at 7:14 PM, Adam Becker notifications@github.comwrote:

My naive opinion:

Trying to use a different markup notation seems like a pain.

contenteditable is fantastic, and anything that's supported in
contenteditable needs to be able to be represented in a ShareJS
document.

Etherpad approaches this by assigning a unique id attribute to each node
in the DOM. Is this a feasible route for ShareJS?


Reply to this email directly or view it on GitHub<
https://github.com/josephg/ShareJS/issues/1#issuecomment-12848938>.


Reply to this email directly or view it on GitHubhttps://github.com/josephg/ShareJS/issues/1#issuecomment-12850694.

mb21 commented Jan 30, 2013

I agree that if this is going to be useful, it needs to work with the contentEditable attribute, i.e. you get an HTML DOM (or string). Otherwise, the editor would have to implement its own layout engine as Google Docs did, which is a non-option for everyone without Google's resources.

The question is whether it's feasible, performance-wise, to parse all the HTML every time a change is made and then transform it to some JSON structure where you can do the OT stuff. The (more complicated) alternative would be to somehow keep track of all (or most of) the HTML elements (e.g. by assigning id attributes), so you could somehow have a pointer from every HTML paragraph to its corresponding JSON object and vice versa.

Contributor

wmertens commented Jan 30, 2013

On Jan 30, 2013, at 10:26 , mb21 notifications@github.com wrote:

I agree that if this is going to be useful, it needs to work with the contentEditable attribute, i.e. you get an HTML DOM (or string). Otherwise, the editor would have to implement its own layout engine as Google Docs did, which is a non-option for everyone without Google's resources.

The question is whether it's feasible, performance-wise, to parse all the HTML every time a change is made and then transform it to some JSON structure where you can do the OT stuff. The (more complicated) alternative would be to somehow keep track of all (or most of) the HTML elements (e.g. by assigning id attributes), so you could somehow have a pointer from every HTML paragraph to its corresponding JSON object and vice versa.

Well, that's pretty much what http://aloha-editor.org/ has to do anyway (minus the OT) so I would say it's not something to worry about.

The bigger problem, like Joseph pointed out, is coming up with an OT for rich-text-represented-as-DOM. E.g. when you make a word in a paragraph bold, you're splitting the paragraph and punching a sub-object in between. When at the same time someone fixes a spelling mistake in that word, that operation needs to be re-pathed into the sub-object.

Wout.

mb21 commented Jan 30, 2013

@wmertens You mean parsing all the HTML is what editors like aloha already do? Yeah, guess you're right. So the currently most promising approach is to parse all the HTML to JSON, then use some modification of the traditional OT on that.

Has anyone read the paper by Davis et al. more carefully than I did? Seems like their data structure is implementable in JSON...

P.S.

However, this makes text operations like searching etc cumbersome.

But you could still do that on the HTML, e.g. TinyMCE has that functionality already.

@bergie bergie referenced this issue in yields/editable May 6, 2013

Closed

undo & redo caret goes out of context. #2

@geigerzaehler geigerzaehler added a commit that referenced this issue Oct 18, 2013

@geigerzaehler geigerzaehler Merge pull request #1 from Dignifiedquire/integration_test
Use grunt-karma and grunt-simple-mocha for running all tests.
90fd304
Contributor

devongovett commented Jan 16, 2014

What's the best way to do rich text today? Has anyone been successful at it?

I've seen several threads on the mailing list as well as this thread, but nothing definitive. To summarize, one implementation using the old ShareJS uses multiple text fragments with the params stored on each one, but it seems unmaintained. There's also the old etherpad type from 0.6 that hasn't made it to 0.7 yet, and I'm not sure how well this works. There's also the Wave model, which I haven't seen anyone port that to ShareJS yet.

It seems like the predominant method people have converged on is to store a string, as well as separate ranges of attributes, kinda like how Apple's NSAttributedString is implemented. Is this a fair characterization? If so, could we just use the JSON type to synchronize this or would a separate type be needed?

I'd really love to see rich text support added to ShareJS, or a separate library on top of ShareJS to add support. If no one else has done this, I'm happy to work on it, I'd just love to see if we might come to a consensus on how best to implement this and to see the work that has already been done by others before I begin. Thanks!

cypha commented Mar 8, 2014

Will 0.7 support rich text? It's the one feature I'm waiting for.

mb21 commented Jun 10, 2014

@devongovett any updates on this? Seems like you were right about storing plain text and formatting ranges separately; e.g. How the Medium Editor Works

Contributor

devongovett commented Jun 13, 2014

@mb21 yeah see the thread on #312. I'm hoping to open source some of my work (at least the OT type if not the rich text editor itself) after the product I'm working on launches hopefully next month. I'll keep everyone posted. 😄

mb21 commented Jun 13, 2014

@devongovett thanks, looking forward to that! So you're using contenteditable to for input but the OT type has plain-text and formatting-ranges separately, right?

Owner

josephg commented Jun 24, 2014

@jhchen is coming by the office tomorrow to chat about getting Quill working with sharejs.

Hey, @josephg, may we know how did the chat with @jhchen go? Any hope getting sharejs working with quill?

@devongovett Any progress on that?

We're using ShareJS for our editor and our basic approach right now is to have a JSON representation of the all block-level elements, with specific operations allowed on them. (Ex. images don't have inserts, just replace URL).

The only issue is with inline formatting (ex. bold). Allowing them via content-editable is (predictably) super buggy when we're using a plain text OT type. Hence our (terrible) solution is to do locking on each block-level element and to apply full-text updates.

Anyone have alternatives at this juncture? Might Markdown provide a solution?

jhchen commented Aug 30, 2014

Joseph and I have been having very productive discussions. The plan is to define rich text ottype that both Quill and ShareJS can use. I'm in the process of making those additions and there will hopefully be something to release soon.

Those are superb news 👍

I have been working on something that may provide a generalized solution to rich-text editing with ShareJS. You can find my project where I do it here: https://github.com/cklokmose/Webstrate

Basically I am storing a representation of the DOM as JsonML in ShareJS and mapping MutationObserver events from the DOM to operations, and operations to changes in the DOM. Hence, it will not only support rich-text editing but editing anything expressed in the DOM (e.g. SVG).

jhchen commented Sep 25, 2014

Hey guys, I pushed some code that is ready for public criticism (which would be very welcome): https://github.com/ottypes/rich-text. There are a few nontrivial todos (filed under its Github issues) but once they are resolved I will update Quill to use this ottype. For now there is a suit of tests (borrowed from another OT library that this and sharejs will succeed) and a passing fuzzer for reassurance that it actually works.

Excellent work. I am no expert in OT but I read everything and all made sense to me. Sorry not to be of more help here.
Once you have it working with Quill I can try to create a Meteor app to test it out.

morgante commented Oct 6, 2014

@jhchen, what's the status on implementing this with Quill? I'd love to integrate this into a project, but it'd be great to see a reference implementation.

jhchen commented Oct 8, 2014

There are some final work I need to do for rich-text but and then I'll implement the changes necessary changes to Quill to use rich-text along with a guide on migrating from the old format to the new.

That's so great @jhchen.

mitar commented Oct 22, 2014

@jhchen: Anywhere to test this new things? I would love to beta test Quill + ShareJS and rich-text editing.

Same here 👍

jhchen commented Oct 22, 2014

Quill has just completely switched over to the rich-text ottype so you can take a peek at the demos there. For example the two editors on http://quilljs.com/examples/ are passing the rich-text back and forth. Hooking everything up with ShareJS is still forthcoming though.

mitar commented Oct 23, 2014

Awesome. This really looks cool. Looking forward to ShareJS integration.

@jhchen I am thinking... if ShareJs is "OT & webshockets" and you have now included OT in Quill... would it be possible to integrate this new Quill with a reactive framework like Meteor which includes webshockets by default, without the need of ShareJS?

Maybe it will be easy to write those OT commands to the mongoDB of meteor and then use the Quill API and the reactivity of meteor to process new OT commands in all connected browsers. What do you think?

jhchen commented Oct 23, 2014

ShareJS is still a crucial piece. rich-text has the primitives (compose, transform) that can be used for OT but there still needs to be some engine to coordinate and utilize all this which is one of ShareJS's roles. This looks to be something meteor is thinking about though: https://trello.com/c/tkBErvIk/39-operational-transformation

mitar commented Oct 23, 2014

Can you point me to where in ShareJS code is that engine? I was looking at that but it seems that ShareJS just pushes all that logic to LiveDB and then leaves to LiveDB to transforms operations?

Could be that server-side logic be extracted somehow so that it could be integrated with Meteor?

@jhchen I'm still new to OT but if you already implemented the OT commands and you can fire events like "text-change" so Meteor can store them as a list in a MongoDB. And then Meteor can send those DB changes to all connected browsers so they trigger the same commands and everything is synchronised... why is still ShareJS crucial? What is left that I am not seeing? Conflict resolution?

mitar commented Oct 24, 2014

No, server does not just redistribute operations, but server is one which decides which operation is primary, and then transforms the others accordingly, and then send things to other clients. See this section on Wikipedia.

mitar commented Oct 24, 2014

So what would be needed, that that transformation engine would be plugged into a Meteor method which is receiving new operation from the client, and then possibly transformed, and then stored into a collection, which is then send to all subscribed clients.

jhchen commented Oct 26, 2014

Yes @mitar is correct. The part of sharejs that does this on the back end is in livedb. I'd also add the client also needs to perform OT and this can be found in the sharejs repo.

mitar commented Oct 26, 2014

Hm, is there a difference between this function on the client and server? Could we use the same function also on the server side in Meteor? Could that function be extracted into some separate module/library?

jhchen commented Oct 26, 2014

Yes ottypes is the common/extracted functionality.

mitar commented Oct 26, 2014

But the code to transform these is not?

jhchen commented Oct 26, 2014

The transform function itself is a part of ottypes (ex. rich-text's). It is the engine's role (ShareJS) to know when to use this. I'm not as familiar with ShareJS's implementation but usually the logic is slightly different between client and server. This is separate from other stuff like transport, messaging, or persistence but I believe this is already extracted from ShareJS (browserchannel, livedb).

mitar commented Oct 27, 2014

So Meteor can provide transport, messaging, and persistence. I would just like to better understand the logic clients and servers have to apply based on message/ops they receive. It seems that ShareJS does transforms at client here, and for server it passes it to the LiveDB?

What I would ideally like is a module/package, which would expose two generic functions: what to do with each op which you get from the server on a client and how it gets transformed so that one can pass this further to Quill. And what to do on the server side when you receive a new op from the client and would like to store that in to the MongoDB (from where other subscription would take an op and send it to other clients).

As @mitar says... with Meteor taking care of transport, messaging and persistence maybe it is not the best idea to use ShareJS anymore because that would duplicate a lot of things.

If I understood it right, what it is left then is the "consistency": how to apply the transform function (which is part of ottypes and not of ShareJS).

BTW, I think the transform logic should be the same both in the client and in the server.
That's the whole point of having a copy of the DB in the browser (minimongo), isn't it?

I am still trying to learn more about OT. I found this interesting article:
http://www.codecommit.com/blog/java/understanding-and-applying-operational-transformation

mb21 commented Oct 27, 2014

btw, check out DerbyJS, which is similar to Meteor, but they actually wrap ShareJS, so a lot of OT stuff should be built in. However, it's still alpha or beta software...

Contributor

devongovett commented Oct 27, 2014

Why would you rewrite ShareJS when you can just use it? If you look at the ShareJS and livedb code, there is A LOT more complexity there than you guys are giving it credit for. There are so many edge cases to handle in this type of system, you're never going to get it right the first time (no offense). At the end of the day, you'll wish you just used ShareJS rather than reinventing the world.

Contributor

devongovett commented Oct 27, 2014

Also, this discussion should be taking place somewhere else (e.g mailing list), not on the rich text support ticket.

Yes, I agree. We could start this conversation in the meteor google group or something.

Owner

josephg commented Oct 27, 2014

👍 Please ask about how sharejs works on the sharejs mailing list instead of in this issue. Your interesting conversation has made a mess of this issue, and now I have the difficult decision of making the issue more useful by deleting the conversation above, or leaving the mess here. I care a lot about this topic, and I'm happy to discuss it - but this isn't the place.

I'm going to answer the question briefly here, and delete any further off-topic discussion on this issue here.

You should think of OT as having two parts: the OT type, and 'concurrency control' (aka everything else).

  • The OT type is like a better diff-match-patch. Unlike diff-match-patch, the OT type is aware of the kind of data you're editing and what the user intention of that change was. This is why we need different code for each type of data. The most important thing the ot type defines is the transform function, which defines what happens when two concurrent edits happen.
  • Concurrency control is all of the rest of a version control system. It decides when to call the transform function, it sends operations between clients (or server/client) and manages the database interaction to save the operations. Sharejs calls transform in both the server and the browser. The code is in livedb lib/ot.js and sharejs lib/client/doc.js.

All the ot types are explicitly defined in the ot types github org with the hope that they can be reused in other ot-compatible tools. I did that specifically so you can reuse them in things like meteor. Please do!

@jhchen jhchen referenced this issue in quilljs/quill Oct 29, 2014

Closed

Server demo #240

wamatt commented Nov 6, 2014

Interesting discussion (esp earlier part of the thread). I realize I'm a bit late to the party and much development has happened, but thought I'd share a thought on rich text OT.

Now this may seem a nuts, but what about mapping Bold, Italics etc to extended characters (sets) in UTF16? This could possibly allow the OT and data side to remain as is. Then in the presentation layer it would render the correct markup based on the character encoding.

Owner

josephg commented Nov 6, 2014

Thats not super clean - you'd still have stray close-bold / open-bold annotation boundaries next to each other in many cases. But that probably doesn't matter. More importantly, it'd be quite hard to embed images, do headings, select fonts and all of that. I think the stuff @jhchen is working on is a better long term solution.

Contributor

devongovett commented Nov 6, 2014

Also, a solution like that would have the side effect of affecting the length of the string, which wouldn't be that desirable. It would make indexing into the string next to impossible without traversing the entire string or some serious caching.

wamatt commented Nov 6, 2014

Yeah ok, you both makes some good points that I hadn't considered. Thanks

mitar commented Nov 6, 2014

One other alternative approach is ConcurrenTree. The basic idea is that instead of having a markup language, you have a parallel array of meta information for each character. So first array is string of characters, the second array is meta information. So first element should be bold, italics, whatever. The interesting trick is that deletion is just marking a character as deleted. So it is really easy to merge concurrent edits because nothing really gets deleted and you can easy "patch" things together. (Of course you can then have periodic compacting to minimize data consumption, if you care about that.)

I find this approach very interesting, but sadly it is not being actively developed.

mitar commented Nov 6, 2014

See also documentation and technical description (and critique of OT).

@larrytin larrytin referenced this issue in polyphony-ot/polyphony-js Nov 12, 2014

Open

Rich text support #2

@jhchen @josephg I might be looking in the wrong place (and sorry if I am!), but have there been any further updates to Quill + ShareJS, or any Rich Text support?

jhchen commented Jan 7, 2015

Rich text support has been added with the rich-text ottype which Quill now uses and is a compatible ottype with ShareJS. Actually hooking everything up in demo/example form is still something I hope to get to at some point.

@jhchen Any update on this? I've been watching your dev branch. Can you give a hint of what solution you were thinking of implementing? I don't need a write up, but if you have a quick example that'd be great. I'm in SF as well if you wanted to meet up and collaborate on this.

Quill is awesome btw! I really appreciate the effort you put into it.

@jhchen I'm feeling a lot like http://xkcd.com/979/ at the moment. Any updates on this? I'm SUPER PUMPED to get QuillJS and ShareJS working together. Thanks for all your work so far!

@jhchen could you _briefly_ explain what steps would need to be done to hook up QuillJs with ShareJs? Just to give me an idea on where to start. Any help is greatly appreciated.

jhchen commented Feb 16, 2015

Most if not all the pieces are there. ShareJS using the rich-text ottype is pretty much everything you need in the back end. Since Quill also uses the rich-text type, you should just be able to listen on text-change events on Quill and just pass those to ShareJS. Similarly pass any updates from ShareJS to Quill's updateContents function.

Last time I tried this though there were synchronization bugs. My suspicion is the cause is due to events being asynchronous and non-blocking in Quill--so you may make an API call to Quill based on contents that are out of date. For example Quill has the contents 'a' and the ShareJS client gets an update. At the same time the user types another letter in Quill. ShareJS doesn't know about the new letter so doesn't do any conflict resolution. The instructions being passed to Quill will be incorrect since ShareJS made them based on an older state of the document. There is a bit of code in Quill that targets this case but it is either not working in all cases or some other module is interfering.

But for a demo the pieces are all there for realtime rich text editing. But for something more production quality I need to find some time to look deeper into this.

butsjoh commented Mar 5, 2015

@jhchen Care to elaborate why quill is using the https://www.npmjs.com/package/rich-text package and not the https://www.npmjs.com/package/ottypes package which sharejs is using?

Maybe you can share :) a light on this as well @josephg

sferoze commented Mar 11, 2015

ShareJS + Quill + Meteor is an awesome powerful combination.

butsjoh commented Mar 11, 2015

@sferoze Care to explain howto ? :)

sferoze commented Mar 11, 2015

@butsjoh oops sorry if I gave the impression that I already figured it out. I am working on it right now. I am going off @jhchen last response and trying it out. Although he mentioned that there were bugs soo it might not be the right way to integrate with Meteor.

Mostly though I am enthusiastic about the combination of Quill, ShareJS and Meteor. Although I don't have it working.

@FredericHeem FredericHeem pushed a commit to FredericHeem/ShareJS that referenced this issue Apr 6, 2015

@noansknv noansknv Merge pull request #1 from noansknv/fix_tests
Fix tests
73f417b

pollen8 commented Apr 7, 2015

I've set up a sample application of how a possible quill/sharejs integration would work. I'm very new to sharejs and quill so may have made some terrible faux pas, so if people want to fork and improve things that would be great: https://github.com/pollen8/plume

@ile ile added a commit to k-archive/k-share that referenced this issue May 18, 2015

@noansknv @ile noansknv + ile Merge pull request #1 from noansknv/fix_tests
Fix tests
Conflicts:
	package.json
2d481a8

@jhchen Just checking for an update on this. I know you are awfully busy. Keep up the good work 👍

+1

Hey, I have a completely different approach to rich text editing. The question I was asking myself when dealing with etherpad and seeing how shareJS wants to integrate rich text support in a similar fashion: Why not sync the DOM directly? Adding support for images, videos and even tables should be a piece of cake, because these things are natural to the DOM.

To try that route I wrote dom-ot, operational transforms for DOM tree operations. And to prove to you that it really works: Have a demo: http://warp.der-analphabet.de

I'm interested to see if this approach is any good and will try to pursue this path further if it is.
Cheers!

Contributor

wmertens commented Jul 4, 2015

Wow nice!

The problem used to be that different browsers produced quite different DOM
when editing, IIRC. Did you test with many browsers? Your solution works
great on Mobile Chrome in any case, unlike many non-wysiwyg editors…

On Sat, Jul 4, 2015, 20:27 Marcel Klehr notifications@github.com wrote:

Hey, I have a completely different approach to rich text editing. The
question I was asking myself when dealing with etherpad and seeing how
shareJS wants to integrate rich text support in a similar fashion: Why not
sync the DOM directly? Adding support for images, videos and even tables
should be a piece of cake, because these things are natural to the DOM.

To try that route I wrote dom-ot https://github.com/marcelklehr/dom-ot,
operational transforms for DOM tree operations. And to prove to you that it
really works: Have a demo: http://warp.der-analphabet.de

I'm interested to see if this approach is any good and will try to pursue
this path further if it is.
Cheers!


Reply to this email directly or view it on GitHub
share#1 (comment).

Wout.
(typed on mobile, excuse terseness)

Hi Marcel,

I’ve been working on something similar, but where I’ve simply mapped changes to the DOM to a JSON representation in JsonML using the Json0 OT type.
You can find it here: https://github.com/cklokmose/Webstrates https://github.com/cklokmose/Webstrates
I’ve written an academic paper about what we have achieved with working on a synchronized and persisted DOM that I will circulate as soon as I have the final version for the publisher ready.

I’d be happy to exchange ideas.

Cheers,
Clemens

On 04/07/2015, at 20.27, Marcel Klehr notifications@github.com wrote:

Hey, I have a completely different approach to rich text editing. The question I was asking myself when dealing with etherpad and seeing how shareJS wants to integrate rich text support in a similar fashion: Why not sync the DOM directly? Adding support for images, videos and even tables should be a piece of cake, because these things are natural to the DOM.

To try that route I wrote dom-ot https://github.com/marcelklehr/dom-ot, operational transforms for DOM tree operations. And to prove to you that it really works: Have a demo: http://warp.der-analphabet.de http://warp.der-analphabet.de/
I'm interested to see if this approach is any good and will try to pursue this path further if it is.
Cheers!


Reply to this email directly or view it on GitHub share#1 (comment).

mizzao commented Jul 23, 2015

A while ago, I integrated ShareJS 0.6 with Meteor: https://github.com/mizzao/meteor-sharejs

It looks like ShareJS 0.7 with Quill is the next target. Having good real-time rich text editing capabilities integrated with the rest of Meteor would be badass.

mitar commented Jul 23, 2015

Yea!

I have created an example of successful live-text editing using Quill on Meteor. This doesn't use ShareJS, just Quill.

https://github.com/jonlachlan/quill-meteor-example

In my particular use-case, I wanted users to have the autonomy to click "Save" to apply their changes, rather than live-editing. And since multiple people might be editing a document at the same time, I didn't want new edits to overwrite another user's work in progress .The template in the example successfully applies changes from down the wire, while maintaining all unsaved edits for the user.

If you want to have live edits (i.e., save as you type), then just uncomment the on text-changes code.

For me, the key insight is client/quill.js line 65:

editor.updateContents(localChanges.transform(remoteChanges, 0));

This uses ottypes (which Quill is built on) to transform the positions of any edits that were made from the server based on what changes were made locally -- this way one set of edits doesn't overwrite another.

This operational transform stuff is quite a bit beyond my experience level, so please approach with all appropriate caveats.

mitar commented Oct 20, 2015

Hm, I think you should also be transforming operations on the server side as well? To make a canonical version there? But I am also not an expert on operational transform.

Right now I'm assuming that the client has the latest version, along with unsaved local edits. That's probably not a good assumption. On the save event, it sends over the whole Delta, not just the changes. So there's a chance that a new update is being sent over the wire, which could get overwritten if the client doesn't register it "in time".

(Edit) So you're right, I think that some way of handling each individual change centrally on the server is the way to go. Perhaps you'd want to extend Minimongo so that you get optimistic updates on the client, then some kind of server process that looks at timestamps etc.

I added a button to the example to demonstrate live editing. It seems like this could be an acceptable level of functionality for many use cases. What do you think?
http://quill-reactive-ot.meteor.com/

mitar commented Oct 21, 2015

I do not think you have to extend Minimongo. Just have custom methods for sending to the server, and then server runs OT operations on its copy, and once it does that, you get a canonical version, which is then pushed to all clients, where clients use OT to resolve conflicts once more.

Also, I think that the design goal of OT was also that you can have delays. So imagine offline use, you edit something without Internet connectivity, you connect back, the idea is that you can reconnect and get all changes in. I do not think that people use it in that way though, I do not think that even Google Docs supports that (but Google Wave did plan to support that).

I just tried a server side method call, but if you type quickly the messages start to get out of order, and on the client these inconsistencies mean that there are incorrect diffs, i.e., if I type "abc" but then I receive from the server "a" then "ac", my client now thinks there's been another edit, so my screen says "acabc", then the "b" message comes in, so now my server says it's "abc" by on my screen I see "abcacabc". At this point if I type another letter "d", then I'm sending "abcacabcd" to the server, because that's what's in my editor.

My next thought is to use a job queue to send the deltas to the server, in order to ensure that the server handles the updates in order. https://atmospherejs.com/?q=queue

mitar commented Oct 23, 2015

I do not think the queue is the problem here. I think server side has simply to apply OT as well. So instead of getting OT messages and just passing them to all clients, server gets OT messages from all clients, applies them to its own version (I do not think order matters), and then send to clients back OT messages based on that version. So in some way if you think about it as diffs. Instead of sending diffs directly between clients (diff between client A version and client A change might not apply cleanly to client B version), you send diff to server (so client A version sends its diff to server which applies, and then sends to client B a diff based on their common history). Something like that.

BTW, for queues I like meteor-job-collection.

How do you ensure that server method calls are processed in order?

mitar commented Oct 23, 2015

I do think they have to be? They have to be in order of each client I think only. And that can be just a incremental counter for each client message?

mitar commented Oct 23, 2015

(BTW, I am far from completely understand OT. This is just what I got when reading around on the web and looking at ShareJS code.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment