question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Native undo on iOS 13

See original GitHub issue

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)
            },
          },
        },
      }),
    ]
  }

}

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:8 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
hanspagelcommented, Jan 7, 2021

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.

0reactions
philippkuehncommented, Dec 3, 2021

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

Read more comments on GitHub >

github_iconTop Results From Across the Web

Native undo on iOS 13 · Issue #468 · ueberdosis/tiptap - GitHub
New tap gestures were introduced for iOS 13 which result in strange undo in TipTap. The following one could be a solution to...
Read more >
Detect iOS 13 undo & redo - Stack Overflow
I have a WKWebView with a custom implementation of undo and redo. I would like to be able to know when the system...
Read more >
How to use 'Shake to Undo' typing on iPhone - Mashable
Shake to Undo is alive and well. But there's always the the finger swipe. ... Mistakes happen. But on the iPhone, they can...
Read more >
How to undo/redo on an iPhone - PhoneArena
The first and probably most well-known way to undo typing on both iPhones and iPads is to simply shake your device. Left, right,...
Read more >
Augmented Text Editing on Mobile: The New iOS 13 Gestures
To undo, swipe to left with three fingers. To redo, swipe to right with three fingers. Sheet Selection. Sometimes you may want to...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found