WKWebview paste with keyboard paste text twice.

In my WKWebview, if I right click (macCatalyst) or long press (ios) paste, it only paste text once. But if I Cmd-V to paste, it will paste text twice with a space between the 2 copied text.

This is my log (canPerform action always return true):

-canPerformAction() action : paste:

-canPerformAction() action : paste:

-canPerformAction() action : paste:

-canPerformAction() action : paste:

-canPerformAction() action : newWindowForTab:

-2021-12-17 09:50:17.101763-0500 myApp[18815:1434268] *** Assertion failure in -[UINSResponderProxy _initWithWrappedResponder:orMenuProxy:forAction:sender:], UINSResponderProxy.m:250

-[UINSResponderProxy _initWithWrappedResponder:orMenuProxy:forAction:sender:]: UINSResponderProxy is improperly wrapping a responder that does not respond to the action.

-canPerformAction() action : paste:

-canPerformAction() action : paste:

-canPerformAction() action : paste:

  • The exact same code for iOS and macOS because Catalyst. Using the keyboard to paste on my iPad does work correctly, but it paste twice on my Mac. I am talking about pasting text inside and input box in a web page running on WKWebview.

  • Did you find a fix for this? I'm experiencing the same issue.

  • No, it is still there.

    Do you also have this problem? https://developer.apple.com/forums/thread/698038

Add a Comment

Replies

I also have the same problem. Hopefully they will fix it fast, there are many problems with the WKWebview that only happens on MAC, it make it almost useless for production use :(

This is still a problem in the latest version of macOS monterey (v12.2). I created a JS solution which overrides the default pasting logic for macOS. Add this JS code as a userscript into your WKWebView user content controller.

Here's the gist link for my code.

I have also attached the code down below:

const inputs = document.querySelectorAll("input[type=text]")
let alreadyPasted = false

for (const input of inputs) {
  input.addEventListener("paste", (event) => {
    event.preventDefault()

    // Don't call paste event two times in a single paste command
    if (alreadyPasted) {
      alreadyPasted = false
      return
    }

    const paste = (event.clipboardData || window.clipboardData).getData("text")

    const beginningString =
      input.value.substring(0, input.selectionStart) + paste

    input.value =
      beginningString +
      input.value.substring(input.selectionEnd, input.value.length)

    alreadyPasted = true

    input.setSelectionRange(beginningString.length, beginningString.length)

    input.scrollLeft = input.scrollWidth
  })
}

Here's a code example to reproduce the double paste bug with MacCatalyst (it doesn't matter if I use SwiftUI, I just like it more)

struct ContentView: View {   
  var body: some View {
    WebView(url: URL(string: "https://google.com")!)
  }
}

struct WebView: UIViewRepresentable {
  let url: URL

  func makeUIView(context: Context) -> WKWebView {
    let webView = WKWebView()
     
    return webView
  }

  func updateUIView(_ uiView: WKWebView, context: Context) {
  }
}

I hope this helps someone out there :)

I tried something like @kingbri's JavaScript hack but the fact the event is only received once when invoked from the menu or other-than-Ctrl-V but twice for Ctrl-V caused problems. I ended up doing the equivalent on the Swift side, which is to override the UIResponderStandardEditActions paste action in my subclass of WKWebView and then avoid calling evaluateJavaScript if there is already a call in progress. When the double paste happens, the 2nd one is bypassed, but when there is only one (or if Apple fixes the bug), only the one is processed. This is what the code would look like if you only want to paste text from the clipboard...

var pastedAsync = false   // A property in the WKWebView subclass

public override func paste(_ sender: Any?) {
    guard !pastedAsync, let text = UIPasteboard.general.text else { return }
    pastedAsync = true
    evaluateJavaScript("YourJSPasteFunction('\(text)')") { result, error in
        self.pastedAsync = false
        // handle the result or error
    }
}

A different approach - subclassing WKWebview, measuring the time between pastes and if they're too close, reject the second attempt.

    class WebView: WKWebView {
        var previousPasteTimestamp: TimeInterval = .zero

        override func paste(_ sender: Any?) {
            let currentPasteTimestamp: TimeInterval = Date().timeIntervalSinceReferenceDate
            if currentPasteTimestamp - previousPasteTimestamp < 0.2 {
                print("prevent double paste")
                return
            }
            previousPasteTimestamp = currentPasteTimestamp
            super.paste(sender)
        }
    }

mackhag, you are our saviour, your solution is elegant, brilliant and simple.

Now please fix the world.

I would recommend that you put that in the code so as to not interfere with the iPad and iPhone version, because this problem does not exist on iOS.

#if targetEnvironment(macCatalyst)

    //Put the mackhag fix here

#endif