Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Native undo on iOS 13 #468

Closed
holtwick opened this issue Sep 30, 2019 · 8 comments
Closed

Native undo on iOS 13 #468

holtwick opened this issue Sep 30, 2019 · 8 comments
Labels
Type: Feature The issue or pullrequest is a new feature

Comments

@holtwick
Copy link

holtwick commented Sep 30, 2019

New tap gestures were introduced for iOS 13 which result in strange undo in TipTap. The following one could be a solution to the problem, maybe worth adding to the core as an option? Original idea found here.

import { Extension, Plugin } from 'tiptap'
import { undo, redo } from 'prosemirror-history'

export default class NativeUndo extends Extension {

  get name() {
    return 'nativeUndo'
  }

  get plugins() {
    let undoMock = null
    let beforeinputHandler = null

    return [
      new Plugin({
        view(view) {

          /**
           * Create a hidden contenteditable element
           * We perform fake actions on this element to manipulate the browser undo stack
           */
          undoMock = document.createElement('div')
          undoMock.setAttribute('contenteditable', 'true')
          undoMock.setAttribute('style', 'position:fixed; bottom:-5em;')
          document.body.insertBefore(undoMock, null)

          const setSelection = range => {
            const sel = window.getSelection()
            const previousRange = sel.rangeCount > 0 ? sel.getRangeAt(0) : null
            sel.removeAllRanges()
            sel.addRange(range)
            return previousRange
          }

          /**
           * By performing a fake action on `undoMock` we force the browser to put something on its undo-stack.
           * This also forces the browser to delete its redo stack.
           */
          const simulateAddToUndoStack = () => {
            const range = document.createRange()
            range.selectNodeContents(undoMock)
            const restoreRange = setSelection(range)
            document.execCommand('insertText', false, 'x')
            setSelection(restoreRange)
            return restoreRange
          }

          let simulatedUndoActive = false

          /**
           * By performing a fake undo on `undoMock`, we force the browser to put something on its redo-stack
           */
          const simulateAddToRedoStack = () => {
            // Perform a fake action on undoMock. The browser will think that it can undo this action.
            const restoreRange = simulateAddToUndoStack()
            // wait for the next tick, and tell the browser to undo the fake action on undoMock
            simulatedUndoActive = true
            try {
              document.execCommand('undo')
            } finally {
              simulatedUndoActive = false
            }
            // restore previous selection
            setSelection(restoreRange)
          }

          beforeinputHandler = (event) => {
            // we only handle user interactions
            if (simulatedUndoActive) {
              return
            }
            switch (event.inputType) {
              case 'historyUndo':
                event.preventDefault()
                undo(view.state, view.dispatch)
                if (undo(view.state)) {
                  // we can perform another undo
                  simulateAddToUndoStack()
                }
                simulateAddToRedoStack()
                return true
              case 'historyRedo':
                event.preventDefault()
                redo(view.state, view.dispatch)
                if (!redo(view.state)) {
                  // by triggering another action, we force the browser to empty the undo stack
                  simulateAddToUndoStack()
                } else {
                  simulateAddToRedoStack()
                }
                return true
            }
            return false
          }

          // In safari the historyUndo/Redo event is triggered on the undoMock
          // In Chrome these events are triggered on the editor
          undoMock.addEventListener('beforeinput', event => {
            beforeinputHandler(event)
          })

          return {
            update(view, prevState) {
            },
            destroy() {
              removeNode(undoMock)
              undoMock = null
            },
          }
        },
        props: {
          handleDOMEvents: {
            beforeinput(view, event) {               
              beforeinputHandler(event, view)
            },
          },
        },
      }),
    ]
  }

}
@holtwick holtwick added the Type: Feature The issue or pullrequest is a new feature label Sep 30, 2019
@philippkuehn
Copy link
Contributor

@holtwick Can you explain the problem in more detail? I'm on iOS13 and do not have any issues.

@holtwick
Copy link
Author

holtwick commented Oct 4, 2019

@philippkuehn It looks as if it would work, but as soon as you do something complex where ProseMirror updates the view it breaks. Example: Type some text, then select bold, type some more and press return type again. Now use 3-finger UNDO and you will get corrupted results.

Video: https://www.icloud.com/photos/#04RavOEUpdU5YT0ONREzXM1pg

@philippkuehn
Copy link
Contributor

oh wow. but shouldn‘t this be part of prosemirror then?

@holtwick
Copy link
Author

holtwick commented Oct 4, 2019

Probably yes. But as you can see from the link above this was originally discussed on the Prosemirror list. Maybe they are not aware of the new problem yet.

@holtwick
Copy link
Author

holtwick commented Oct 6, 2019

Another iOS 13 related issue: ProseMirror/prosemirror#982

@holtwick
Copy link
Author

holtwick commented Oct 8, 2019

but shouldn‘t this be part of prosemirror then?

@philippkuehn, see this answer from @marijnh: ProseMirror/prosemirror#684 (comment)

@hanspagel hanspagel added the v2 label Jan 7, 2021
@hanspagel
Copy link
Contributor

Thanks @holtwick! I’m quoting him here directly:

Putting code like this into the history package and exporting it as a separate plugin might be a good idea. I think it's too terrible a hack to enable by default, but it might be useful to a lot of people.

@hanspagel hanspagel reopened this Jan 7, 2021
@hanspagel hanspagel removed the v2 label Jan 7, 2021
@hanspagel hanspagel added the v1 label Mar 29, 2021
@hanspagel hanspagel removed the v1 label Sep 28, 2021
@philippkuehn
Copy link
Contributor

Closing this for now because I also think it’s a bit too hacky :)

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

No branches or pull requests

3 participants