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

Removal of browser built-in Undo stack functionality from contenteditable #150

Open
johanneswilm opened this Issue Sep 24, 2016 · 37 comments

Comments

Projects
None yet
6 participants
@johanneswilm
Copy link
Contributor

johanneswilm commented Sep 24, 2016

The subject of removing the undo manager functionality from contenteditable by default came up at the Editing Taskforce F2F meeting on 2016-09-22 at TPAC in Lisboa, Portugal. It was pointed out that the browser's undo stack would be entirely useless once the JS editor interrupt the default behavior even in just a few limited cases. No-one present opposed the idea of removing the browser's undo functionality for contenteditable and replacing it by a way for the JS to enable/disable the menu entries for redo and undo + giving the ability to listen for redo/undo being triggered via beforeinput events.

Everyone agreed to turn this functionality of by default. (given that there seems to be no authoritative spec that says that undo IS provided, we may only need to spec that undo is turned off be default).

In addition, the following JS editor projects were contacted and all of them responded that the browser's undo stack and default undo behavior was either useless or harmful:

  • CKEditor
  • ProseMirror
  • QuillJS
  • ContentTools library
  • Substance.io
  • TinyMCE
  • Froala

The following applications were investigated and it was determined that they use their own undo stacks (a separate stack for each input field):

  • Medium.js
  • Google Gmail
  • Microsoft Office 365
  • Apple iCloud Pages
  • Google Docs (only using very limited amount of contenteditable anyway)
  • Facebook's various input fields
  • Wikimedia Visual editor

One working contenteditable webapp was found that makes use of the browser's undo stack:

  • Facebook's draft-js uses its own undo stack, but falls through to the native undo stack in case the last change was a spell checker fix, so that the browser learns that the change was unwanted and stops making the same suggestion in the future [1].

[1] https://github.com/facebook/draft-js/blob/67c5e69499e3b0c149ce83b004872afdf4180463/src/component/handlers/edit/commands/keyCommandUndo.js#L24-L27

(This issue is updated as responses from developers arrive.)

@rniwa

This comment has been minimized.

Copy link
Contributor

rniwa commented Sep 24, 2016

Disabling undo/redo is possible but enabling undo/redo would require each app inserting an undo item into the browser's undo stack (NSUndoManager) on Safari (both Mac & iOS). This is precisely why the old undo manager spec, which Gecko implemented at some point, had an explicit mechanism to insert and remove undo entries.

@johanneswilm

This comment has been minimized.

Copy link
Contributor Author

johanneswilm commented Sep 24, 2016

@rniwa On iOS/Safari, what are the ways that undo/redo can be activated? The issue of having to have an item in the undo stack in order to have the menu enabled is something that is part of the OS, somehow?

Is there absolutely no way that Safari could take care of inserting this token item into the undo stack when there is focus on the contenteditable element and undo/redo is to be enabled and remove that item again when moving the focus? That would be preferable, as that way the JS editor developer doesn't have to program something specific just for Safari.

The browser would still take care of undo-handling of textareas and input=text elements.

@rniwa

This comment has been minimized.

Copy link
Contributor

rniwa commented Sep 24, 2016

Could Safari take care of inserting this token item into the undo stack when there is focus on the contenteditable element and undo/redo is to be enabled, and then somehow also empty the entire undo/redo stack when it has to be disabled?

Not really. Clearing undo stack is possible but then we'd have no way of inserting undo items back. I investigated this problem four years ago. Please go ahead and read old discussions on public-webapps.

Web apps really need to be using browser's undo stack. Also, talking about enabling / disabling menu item is extremely shortsighted approach. There is a reason some operating system provide a high-level abstraction for undo/redo, and any approach that relies on some existent UI tends to be not forward compatible. For example, how is an assistant technology supposed to expose undo/redo stacks if the only thing the browser knows is that there is something undoable? We need to be able to describe to the user what undo does.

@johanneswilm

This comment has been minimized.

Copy link
Contributor Author

johanneswilm commented Sep 24, 2016

Web apps really need to be using browser's undo stack. Also, talking about enabling / disabling menu item is extremely shortsighted approach. There is a reason some operating system provide a high-level abstraction for undo/redo, and any approach that relies on some existent UI tends to be not forward compatible.

The issue here is that the browser's undo-stack is unusable in the case of all editors that use even just a little bit of JavaScript to manipulate the DOM due to user intentions. Given that raw contenteditable doesn't work, this includes all existing editors. Some editors are reporting that they need to "fight" the browser in it's attempt to undo itself. Others are apparently intercepting the keyboard shortcuts and the menu items are just not working.

So the question is not about throwing out a technology that is working. It is about removing something that is entirely unusable and replacing it with something that reduces the number of unused/non-working menu entries.

For example, how is an assistant technology supposed to expose undo/redo stacks if the only thing the browser knows is that there is something undoable?

That may be a good point. Do we have specific requirements for what has to be known for these technologies? Is it more than a title for what the next redo or undo would do?

We need to be able to describe to the user what undo does.

This is part of the Apple platform requirements or of assistant technologies?

@johanneswilm

This comment has been minimized.

Copy link
Contributor Author

johanneswilm commented Sep 24, 2016

Having the webapp add and remove items from a browser-held undo history may work, if they can add something like a dictionary of their own data. Changing the DOM automatically is not something the browser should be engaged in.

Also, it needs to be able to add and remove great numbers of items to the undo and redo stack at arbitrary positions. For use cases such as;

User A and B are writing together in a collaborative editor. Their document is 3 paragraphs long. User A types 3 words in paragraph one, 10 words in paragraph two, and 4 words in paragraph three.User B then deletes paragraph two. The undo stack for User A should now contain 7 items, and that of User B should contain 1 item. If User B undos once, the undo stack of User A should contain 17 items, and that of User B should contain 0 items.

@johanneswilm

This comment has been minimized.

Copy link
Contributor Author

johanneswilm commented Sep 24, 2016

Testing: Undo/redo browser native menu items don't work at all in Google Docs and Office 365 on Safari/Mac OS X.

Do these applications work with assistant technologies?

@johanneswilm

This comment has been minimized.

Copy link
Contributor Author

johanneswilm commented Sep 24, 2016

@rniwa I have been looking through old discussions, but all I can find is a system whereby the browser changes the DOM and automatically applies DOM changes. That seems to be what we want to get away from now.

@rniwa

This comment has been minimized.

Copy link
Contributor

rniwa commented Sep 26, 2016

The issue here is that the browser's undo-stack is unusable in the case of all editors that use even just a little bit of JavaScript to manipulate the DOM due to user intentions.

The issue is well understood. The problem is that your proposed solution is not implementable.

Also, it needs to be able to add and remove great numbers of items to the undo and redo stack at arbitrary positions.

Removing and inserting arbitrary number of items is possible but not at arbitrary positions for the same reason we can't just enable/disable undo/redo. In order to do that kind of manipulation, one has to remove all entries up to the entry to be removed, and re-insert all succeeding entries.

@rniwa I have been looking through old discussions, but all I can find is a system whereby the browser changes the DOM and automatically applies DOM changes. That seems to be what we want to get away from now.

The old undo manager API did support automatically rolling DOM changes, but also provided a mechanism to insert an entry without any DOM mutations that get automatically unapplied & reapplied. Basically, you just create a DOM transaction, don't do anything inside the transaction, and insert it to the undo manager. Because a transaction can be associated with any JS data, and undo / redo events will be fired upon unapplying and reapplying those transactions, scripts can do whatever they want to do upon receiving those events.

@johanneswilm

This comment has been minimized.

Copy link
Contributor Author

johanneswilm commented Sep 26, 2016

The issue is well understood. The problem is that your proposed solution is not implementable.

You seem to be the first browser person saying this, so I would like to find out what exactly is hindering this from happening. Maybe it's just browser developers who have to rethink about how some things can be put together. Or maybe we need to reformulate this slightly to get around restrictions browser makers face.

Removing and inserting arbitrary number of items is possible but not at arbitrary positions for the same reason we can't just enable/disable undo/redo. In order to do that kind of manipulation, one has to remove all entries up to the entry to be removed, and re-insert all succeeding entries.

Ok, well that means it's kind of useless. What exactly is hindering browsers from creating two new menu entries that have nothing to do with the existing undo/redo functionality that are labeled "undo" and "redo" which are shown whenever the selection is in a contenteditable area?

The old undo manager API did support automatically rolling DOM changes, but also provided a mechanism to insert an entry without any DOM mutations that get automatically unapplied & reapplied.

I've had a look at it. So far I have not been able to find any JavaScript project that would seem to have any use of it. It would just mean they have to spend a lot of time implementing something that gives them no advantage.

I think we need to look at what advantages assistant technologies have from using the current native undo stack, and then look at how we can give access to that to the JavaScript undo stacks editors are using anyway.

@rniwa

This comment has been minimized.

Copy link
Contributor

rniwa commented Sep 26, 2016

The issue is well understood. The problem is that your proposed solution is not implementable.

You seem to be the first browser person saying this, so I would like to find out what exactly is hindering this from happening. Maybe it's just browser developers who have to rethink about how some things can be put together. Or maybe we need to reformulate this slightly to get around restrictions browser makers face.

We've gone though that exercise four years ago. I find your dismissal attitude with regards to all these discussion extremely offensive. I'm gonna stop following this discussion because it's really affecting my well being at this point.

@johanneswilm

This comment has been minimized.

Copy link
Contributor Author

johanneswilm commented Sep 26, 2016

I am not trying todismiss anything. I looked through the old spec and I searched the email list. Maybe you could provide me with a link?
I think what's new now is that we actually have JS editor and browser developers come together and try to find a solution for the problems editors run into. That's fairly new. Now we just need to find away around the hacks editor developers run into.

@johanneswilm

This comment has been minimized.

Copy link
Contributor Author

johanneswilm commented Sep 26, 2016

Another problem with the undo manager proposal seems to be that it only allows one, global undo stack. This is not how applications like Google Docs and Office 365 work as of today. The same is true for all the other editor projects as they all have their own undo stack.

@johanneswilm

This comment has been minimized.

Copy link
Contributor Author

johanneswilm commented Sep 26, 2016

@rniwa: What part exactly is the impossible? Is it

A) Allowing JS to disable the undo/redo menu entries? As I understood your proposal, this wasn't an issue.

B) Enabling the undo/redo items if JS requests this. If the menu entries are directly linked to the OS, this could be done by adding fake items there if the stacks are empty and removing them when the editor loses focus. Given that the global undo stack is broken anyway, this shouldn't matter, should it?

C) Triggering a beforeinput event for undo/redo?

D) Not changing the DOM be default if beforeinput isn't canceled? This seems like it would be a minor issue.

@rniwa

This comment has been minimized.

Copy link
Contributor

rniwa commented Sep 26, 2016

B. Adding a fake item, etc... doesn't work because of the way it integrates with the rest of the browser.

Mutating DOM is not necessary at all. It was merely a convenience feature we wanted to add at the time.

The bottom line is, the only way we can support something like this would be JS authors pushing and popping an entry into and from the native undo stack we manage. We can provide an API, etc... to associate and dissociate arbitrary JS data.

The thing is I've stated our requirements. I'm not going into details of what is and what is not possible with the native API because such is an outside the scope of the standardization process. I'm simply giving an implementation feedback that what you proposed is not implementable, and I've proposed an alternative, which gives full control of DOM mutations and allows authors to remove/add their own undo entries so I don't know what else I can say.

@johanneswilm

This comment has been minimized.

Copy link
Contributor Author

johanneswilm commented Sep 26, 2016

Ok, but if I understand the alternative correctly it:

  • Imposes having a global undo stack for all input fields on one page, which is not what webapps currently do, not even those by browser vendors such as Google or Microsoft.
  • Doesn't work with collaborative editors where an arbitrary number of items have to be added or removed from some point in the past.
  • If one imports 1000 undo items from previous doc edits that were stored on the server, it means that the history of all other input fields is unreachable.

But how about this: If you implement that undomanager, it should allow JS to push one fake undo step and one fake redo step when the focus is on the editor and then to consume those when the focus moves away, right? So it would basically be the same thing with the only difference being that JS needs to add 1-3 extra lines of code for Safari. I think that should work!

@johanneswilm

This comment has been minimized.

Copy link
Contributor Author

johanneswilm commented Sep 26, 2016

I guess this can be done even now, as long as Safari supports execCommand, by executing two commands, then undoing one, then putting the DOM back to its initial state by manipulating it directly in JS. Now both menus should be enabled. It's a bit of a hack, but it would be a solution that would make beforeinput useful for undo/redo immediately when beforeinput has been implemented in Safari. Solving it all in a non-hackish way could be a next step.

@rniwa

This comment has been minimized.

Copy link
Contributor

rniwa commented Sep 27, 2016

Imposes having a global undo stack for all input fields on one page, which is not what webapps currently do, not even those by browser vendors such as Google or Microsoft.

Chrome certainly shares a single undo stack for the entire page. You can test this by typing text into a text field across iframe boundaries: https://jsfiddle.net/buny13mz/

Changing this behavior in regular case (i.e. ones that doesn't invoke custom editor) is unacceptable because it breaks a key browser UX.

Doesn't work with collaborative editors where an arbitrary number of items have to be added or removed from some point in the past.

When we designed the old Undo Manager API, we specifically worked with Google Docs team to ensure their undo worked with the API so this assertion is false.

If you implement that undomanager, it should allow JS to push one fake undo step and one fake redo step when the focus is on the editor and then to consume those when the focus moves away, right? So it would basically be the same thing with the only difference being that JS needs to add 1-3 extra lines of code for Safari.

Not quite. This would still break semantics of the shared undo stack across documents. So you'd have to insert a fake undo/redo for the collaborative undo/redo part at the beginning, and then insert new undo/redo entry as the current client modifies content. You'd just ignore callbacks and pop more entires when undo/redo fires on things that were supposed to be removed earlier. You don't have to actually remove it from the browser's undo stack as they happen.

@johanneswilm

This comment has been minimized.

Copy link
Contributor Author

johanneswilm commented Sep 27, 2016

Imposes having a global undo stack for all input fields on one page, which is not what webapps currently do, not even those by browser vendors such as Google or Microsoft.

Chrome certainly shares a single undo stack for the entire page. You can test this by typing text into a text field across iframe boundaries: https://jsfiddle.net/buny13mz/

Yes, but that's the undo stack noone uses for richtext beyond demo purposes. If you go to actual applications Google has made, like Gmail or Google Docs, you'll see that they have implemented it with a different undo stack for each input field.

Changing this behavior in regular case (i.e. ones that doesn't invoke custom editor) is unacceptable because it breaks a key browser UX.

But every time contenteditable is involved, a custom editor will be involved. And all of them create their own undo stack.

The place where the global undo stack still may be used is if you have a series of input text fields in a form. For that case it's fine. I am only talking about contenteditable here.

Doesn't work with collaborative editors where an arbitrary number of items have to be added or removed from some point in the past.

When we designed the old Undo Manager API, we specifically worked with Google Docs team to ensure their undo worked with the API so this assertion is false.

But if I understood you correctly, you said that it cannot include add and remove undo steps at an arbitrary point in the past? (usecase: #150 (comment) )

Not quite. This would still break semantics of the shared undo stack across documents.

The global undo stack is already broken as it is -- in all of the web apps, because it's global, because changes are added via JS, etc. . As long as contenteditable adds items to this global undo stack, it will be broken. That's what this issue was about. Your undo manager may still be useful for other areas - form filling, textareas, SVG editing, etc, so I'm not suggesting you stop working on it. It's just not the solution for richtext editing.

I figured out how to make sure that both undo and redo are enabled in Chrome and Firefox. It doesn't quite work in Safari, because Safari seems to join several edit operations into a single item in the undo stack, but with a few hours of trial and error, I'm sure JS developers could find a solution there as well:

<!DOCTYPE html>
<html>
<head>
<script>
document.addEventListener("DOMContentLoaded", function(event) {
  // Enable undo and redo menu entries
  var undoEl = document.createElement('div');
  undoEl.setAttribute('contenteditable','true');
  undoEl.innerHTML = "b";
  var undoElText = undoEl.firstChild;
  document.body.appendChild(undoEl);
  var r = document.createRange();
  r.setStart(undoElText, 0);
  r.setEnd(undoElText, 1);
  var s = window.getSelection();
  s.removeAllRanges();
  s.addRange(r);
  document.execCommand('bold');
  document.execCommand('bold');
  document.execCommand('undo');
  document.body.removeChild(undoEl);
});
</script>
</head>
<body>
<div contenteditable="true">
<h1>Title</h1>
<p>Text...</p>
</div>
</body>
</html>
@johanneswilm

This comment has been minimized.

Copy link
Contributor Author

johanneswilm commented Sep 27, 2016

Not quite. This would still break semantics of the shared undo stack across documents.

The global undo stack is already broken as it is -- in all of the web apps, because it's global, because changes are added via JS, etc. . As long as contenteditable adds items to this global undo stack, it will be broken.

The other option---and this is what was proposed by Chrome and Edge representatives at the F2F---is to exclude contenteditable from the global history and add a way to enable/disable the undo/redo menu entries so that the editor can receive the corresponding beforeinput event.

That is of course a much cleaner solution as it doesn't interfere with the global undo stack used by other fields. I opened this issue primarily to figure out if there is actually no use for the global undo stack at all. And there I found the draft-js issue which probably needs further investigation.

Now if Safari cannot do what was proposed by the other browser vendors, then we need a solution for that. At this early stage, it maybe enough to find a JS hack to get around the disabled undo/redo menu items. At the next stage, I would think it would make sense to find some less hacky solution, but we can talk about it at that time.

@chaals

This comment has been minimized.

Copy link

chaals commented Sep 27, 2016

The problem with not integrating into the global undo stack is that users have historically been given only one stack. And their memory of what they have done doesn't usually distinguish between different input fields.

This is changing, because as Johannes says, editing systems are making a custom undo stack per editable region.

There are also systems that offer more information about what "undo" will actually do - which is good because it isn't often very clear. In particular, it is sometimes very hard to know what can be undone, and what cannot.

It would be good to get telemetry from editor apps as well as browsers on how often, and how far, people actually use undo…

@johanneswilm

This comment has been minimized.

Copy link
Contributor Author

johanneswilm commented Sep 27, 2016

The problem with not integrating into the global undo stack is that users have historically been given only one stack. And their memory of what they have done doesn't usually distinguish between different input fields.

True, although we may have to go a while back. Before working on this, I wasn't aware that there was something like a global undo stack as I had only ever had to deal with undo stacks per editable element (an exception being editable elements contained in other editable elements). Maybe we could talk to Google Docs, Office 365, Gmail & others who "voluntarily" chose to switch toward having multiple undo stacks and find out how long time ago they made this decision?

There are also systems that offer more information about what "undo" will actually do - which is good because it isn't often very clear. In particular, it is sometimes very hard to know what can be undone, and what cannot.

This is also where assistant technologies come in? Do we have a description ok what exactly they need? For example: do they need a written description and possibly type for the next possible undo or redo step, or do they need to go even further back?

It would be good to get telemetry from editor apps as well as browsers on how often, and how far, people actually use undo…

I think we have already taken one step in the right direction: focusing on editing apps and their developers. Going all the way to the end user would make the process of figuring out what we need to provide even more perfect. But it may not be without problems.

I wonder what type of data actually exists on this. The first post here is a result of me sending out a simple one-question survey to the various editor apps. Additionally, I looked at the source code or tried the app in the case of the editor apps where I couldn't get a response or didn't know who to contact.

Maybe we should design a more complex survey which also includes questions about their end users and how much they know about them. They may not be willing to share much more than their final conclusion though, given that they are competing with oneanother.

Of course, with every new survey there is the chance that people don't bother answering us.

@johanneswilm

This comment has been minimized.

Copy link
Contributor Author

johanneswilm commented Sep 27, 2016

I just tried iCloud Pages on Safari/Mac OS X, and it also maintains different undo stacks for each input window (font size, comment, main doc). The browser's redo/undo menu entries are not working (same as Google Docs and Ms Office 365). Instead it provides undo/redo buttons in the apps UI that switch whenever the user switches from one input box to the next.

@rniwa I'm trying to understand Apple's overall UX policy in this area. Is the rule you mention about having one global undo stack something that only applies for pages where the editor is not the main element? Wouldn't it be good to provide menu entries that would allow iCloud Pages to function as it does now, but with working Undo/Redo buttons? It Apple's policy requires having one global undo stack on some types of pages, should we maybe look for a solution that allows for both a global undo stack and one that has separate undo stacks?

@rniwa

This comment has been minimized.

Copy link
Contributor

rniwa commented Sep 27, 2016

Wouldn't it be good to provide menu entries that would allow iCloud Pages to function as it does now, but with working Undo/Redo buttons? It Apple's policy requires having one global undo stack on some types of pages, should we maybe look for a solution that allows for both a global undo stack and one that has separate undo stacks?

Right, this is likely to be implementation-dependent behavior like focus and selection. The key here is to allow browser vendors to implement either behavior and let web authors choose which behaviors they want. This is why the old undo manager API had undoscope attribute.

The only problem with the old API was that being able to add a new undoscope via a content attribute made things way too complicated for implementors. I think a better solution would be adding a flag on ShadowRoot. Since builtin text fields are modeled like they have their own shadow root, any UA that uses a separate undo stack per text field (e.g. Gecko) can be modeled using this way.

@johanneswilm

This comment has been minimized.

Copy link
Contributor Author

johanneswilm commented Sep 27, 2016

Right, this is likely to be implementation-dependent behavior like focus and selection.

Implementation as in "webpage app implementation" or as in "UA implementation"? As far as I can tell, all these JS editors on all the platforms, have separate undo stacks for each input field.

We may need to look at that a bit more in details: Some of these apps are "forced" to have a separate undo stack, because the current one is useless. This would be the case for the smaller editors that can be one of several components on a webpage. The other ones "voluntarily" have separate undo stacks. This would be the case of the bigger webapps that fill the entire page with various input boxes which they all all control. They could have created one global undo stack for all their input fields, but instead they chose to have separate undo stacks for each input field - this is the case with the various products of Microsoft, Apple, Google, etc. .

The big question is here: Would there be simple a richtext editor that would prefer the global undo stack, if they could? From their responses, I haven't come across such an editor project, but that doesn't mean they don't exist.

In any case, we should plan for the separate undo stack, because this seems to be the main usecase and defacto standard as of now.

If the undo manager can be adjusted to accommodate for local undo stacks and be implementable -- that's great. The question is then just what the advantage for the web developer would be to use this undo manager. If the web developer can add two "fake" edit operations to an undo stack that is local to just one element, so that the undo/redo buttons are shown, and then intercepts all edit operations with beforeinput, that should work without contaminating the global undo stack, right?

Personally I wouldn't mind using a built-in undo manager. But as soon as it restricts me in any way even with a more exotic operation, I would likely away from it rather immediately, at least if there is no perceived advantage with using it.

@rniwa

This comment has been minimized.

Copy link
Contributor

rniwa commented Sep 27, 2016

In any case, we should plan for the separate undo stack, because this seems to be the main use case and defacto standard as of now.

We're fine with having that as an option but forcing that on all contenteditable content is definitely not okay for us.

@johanneswilm

This comment has been minimized.

Copy link
Contributor Author

johanneswilm commented Sep 27, 2016

In any case, we should plan for the separate undo stack, because this seems to be the main use case and defacto standard as of now.

We're fine with having that as an option but forcing that on all contenteditable content is definitely not okay for us.

Great! Then we are in full agreement.

Then we just need to figure out how to do it in combination with the beforeinput system and the inputTypes undo and redo. Say we simply combine beforeinput with a future undomanager that always for the insertion of arbitrary JS objects to be stored with the undo entry. Undert these circumstances, what kind of data should we get in the beforeinput events for undo and redo? Should we simply get access to the item in the undo stack that would be applied if we don't preventDefault it?

And can we have an intermediate solution until we get the undo manager so that we can ensure that undo and redo always are enabled when we need them to? Or should we just say that JS developers simply need to use a hack as I posted above, in the meantime?

@rniwa

This comment has been minimized.

Copy link
Contributor

rniwa commented Sep 28, 2016

Again, the problem here is that there is no intermediate solution for us. We can't arbitrarily enable undo/redo menu item in Safari without having the capability to push/pop undo stack items.

@waterplea

This comment has been minimized.

Copy link

waterplea commented Jan 29, 2019

A much better solution to removing native Undo/Redo from contenteditable would be endorsing what I just uncovered IE/Edge had for ages:

document.execCommand('ms-beginUndoUnit');
contentEditableElement.innerHTML = '';
document.execCommand('ms-endUndoUnit');

This way you can define particular manipulations as a singular Undo unit.

@johanneswilm

This comment has been minimized.

Copy link
Contributor Author

johanneswilm commented Jan 29, 2019

@waterplea That is very interesting! Undo not recognizing changes made by other parts of JavaScript is only one part of the problem though. The other part is that it has one global undo stack for all the elements on the same page. That goes against how every app I have been to find works. Undo stacks work on a per element basis in reality. It's a really odd choice to have the entire page share the same undo stack, which is probably why noone has thought of testing for this.

@waterplea

This comment has been minimized.

Copy link

waterplea commented Jan 29, 2019

Well, what I did to circumvent that was creating an iframe with a designMode = 'on' document inside. This way I have a separate Undo stack for an editor.

@Reinmar

This comment has been minimized.

Copy link

Reinmar commented Jan 29, 2019

I don't think it would work with real-time collaborative editing (where you can only undo your changes). Such a way of communicating with the browser could help, but something more straightforward would be better.

@rniwa

This comment has been minimized.

Copy link
Contributor

rniwa commented Feb 3, 2019

Well, what I did to circumvent that was creating an iframe with a designMode = 'on' document inside. This way I have a separate Undo stack for an editor.

Note that WebKit famously (?) does not have a undo stack per document. Safari's undo stack is shared across all the documents. In general, the behavior about undo/redo isn't really standardized and diverges across platforms.

A while ago, Google proposed on a real UndoManager API (I say Google because I was working at Google at the time) but it didn't get much traction due to its immense complexity, etc...

@whsieh and I are experimenting with something simpler where you can just register a function for undo / redo. I think real-time collaborating editing is a challenging case regardless because what you may have been previously undoable may no longer become undoable at some point.

However, we believe having some good undo / redo on the Web is important since undo / redo is one of the most frequently used feature for any real productivity app, and not having any browser integration would mean that assistant technology (for accessibility) can't even recognize that there is something undoable. It's a freaking embarrassment that we're in such a broken state in 2019.

@Reinmar

This comment has been minimized.

Copy link

Reinmar commented Feb 3, 2019

@whsieh and I are experimenting with something simpler where you can just register a function for undo / redo.

Sounds interesting! Could you share some more details?

I think real-time collaborating editing is a challenging case regardless because what you may have been previously undoable may no longer become undoable at some point.

Typically, a rich-text editor which supports operational transformation or other real-time collaborative engine will have its own undo manager and know what can be undone (usually, the current user's changes only). So as long as the decision is in JS editor's "hands" we should be fine.

@johanneswilm

This comment has been minimized.

Copy link
Contributor Author

johanneswilm commented Feb 3, 2019

Typically, a rich-text editor which supports operational transformation or other real-time collaborative engine will have its own undo manager and know what can be undone (usually, the current user's changes only).

Additionally, the undoing may not correspond exactly to what had been changed in the first place.
So for example if you have something like:

User 1 writes a word:

The fish are exppensive|.

User 2 corrects the word:

The fish are exppensive.

User 1 undoes his latest change. This can either be entirely forbidden, as it interferes with the changes of user 2, or it could for example be to delete the new word:

The fish are expensive.

What is possible is a decision that has to be taken fairly close to the final app user / person who runs the website as there are many factors to that decide what makes most sense. It can have to do with the kinds of users and the kinds of texts one is dealing with.

So as long as the decision is in JS editor's "hands" we should be fine.

Agreed. Very important that we leave the decision to JS as otherwise it will likely just be ignored/overridden as is the case today.

@whsieh

This comment has been minimized.

Copy link

whsieh commented Feb 4, 2019

@whsieh and I are experimenting with something simpler where you can just register a function for undo / redo.

Sounds interesting! Could you share some more details?

Sure! Here's a high-level overview; however, please note that the UndoManager prototype is currently only enabled in some first party Apple applications, and is not available in Safari quite yet.

There's a couple of main parts to this API: UndoManager and UndoItem. The UndoManager is exposed as a readonly attribute on Document, and has a method, addItem, that can be used to add an undoable action to the platform undo stack. This undoable action is backed by an UndoItem, which may be constructed using a label string and callbacks that are invoked when the user triggers undo or redo.

interface UndoManager {
    void addItem(UndoItem item)
};

...and UndoItem is defined as such:

interface UndoItem {
    Constructor(UndoItemInit initDict);
    readonly attribute DOMString label;
};

dictionary UndoItemInit {
    required DOMString label; // Determines the string to display in platform undo UI (for instance, the menu bar on macOS).
    required VoidCallback undo; // Invoked when the user triggers undo.
    required VoidCallback redo; // Invoked when the user triggers redo.
};

The hope is to make it easier for web content to define custom undoable actions using DOM API, in a way that:

(1) doesn't require maintaining a custom notion of an undo stack in script, and...
(2) maintains better platform integration by influencing the undo label name in platform UI, as well as being agnostic to all platform-specific methods of triggering undo and redo (e.g. shaking the device on iOS, and selecting actions in the menu bar on macOS).

@johanneswilm

This comment has been minimized.

Copy link
Contributor Author

johanneswilm commented Feb 4, 2019

Here's a high-level overview

This looks generally good. The one case it doesn't seem to be able to handle is when a specific action becomes un-undoable after the fact, right?
Also, you keep the global undo stack for all apps on the same page which goes against the logic of all richtext editing web apps we have been able to find. This second point may not be that important though if it really can be made to work reliably in most cases where is only one big editor on a page and maybe 1-2 small input elements. We'd have to see how end users respond to a global undo stack like that.

@waterplea

This comment has been minimized.

Copy link

waterplea commented Feb 4, 2019

@rniwa you are right, Safari does not isolate undo stack per document. That's unfortunate. Thankfully the audience of my app rarely uses Safari so sometimes graceful degradation works for me. As for Chrome — iframe isolates Undo stack well on it, as it does on Firefox and IE/Edge.

I'm building a humble WYSIWYG editor, mostly basic editing and no collaboration. With those poorly documented IE commands for defining undo item I've managed to create native polyfill for insertHTML/insertText. Accessibility is my main concern, that's why I dance around native Undo stack rather than making my own like more mature editors do. After all, you can redefine undo actions (at least on Mac I believe) so there's no secure way of making your own undo stack work 'natively' that I know of. Add to that other ways of triggering mentioned above.

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