Preserve text selection on click in Safari

Viewed 240

Selecting some text on a page and then clicking a button preserves text selection in pretty much any browser other than Safari (at least its mobile version).

Is there any way to make Safari behave just like everything else?

Simple jsFiddle demonstrating the problem, where selecting some text and clicking the button doesn't report selected text in Safari (selection is removed and window.getSelection() returns nothing): https://jsfiddle.net/u5b3awhd/

    <span>here's some text to select</span>
    <button onclick="showSelection()">click me</button>
    <div id="message"></div>
    function showSelection() {
      document.getElementById("message").innerText = "selected " + window.getSelection();
    }

I've tried adding user-select: none and similar styles to the button, but it didn't help.

I also found some recommendations to get selected text on some other input events (mouse events etc.), but there are 2 major reasons I'm looking for a better solution:

  1. It looks a little hacky, especially since this needs to run on mobile, where there is no mouse, so those events are emulated and might be tricky to get right without other undesirable side effects. Although the selectionchange event might partially solve this problem, there is still another problem #2...
  2. Text selection in browser UI actually gets lost and the user needs to select it again later, which is not the best experience, especially considering that it works great in other browsers.

So how does everyone solve this in 2022?

1 Answers

I've got a solution for you, maybe, it feels gross because we're certainly compensating for the weirdness of iOS Safari. It really depends on what you're doing with the selection. Specific to your test code above:

var documentFragment;
var ancestorNode;

// listen to touchy on the element you want to select from
document.querySelector("span").addEventListener("touchend", (e) => {
   let selection = document.getSelection();
   if (!selection || selection.rangeCount === 0 || selection.getRangeAt(0).length === 0)
      return;
   let range = selection.getRangeAt(0);
   ancestorNode = range.commonAncestorContainer; // if you need it
   documentFragment = range.CloneContents(); // because you'll never get the selection
});

document.querySelector("button").addEventListener("click", (e) => {
   if (!documentFragment) {
      // nothing there from a 'touchend' event, check now from other means.
      // do all the things above, but you know, refactor it so it's DRY.
   }
   if (documentFragment) {
      // do all the things with the fragment, like...
      let div = document.createElement("div");
      div.appendChild(documentFragment);
   }
});

Even though the selection is cleared, what it was is now contained in that object. It may be more elegant, and work, if you listen to touchend and mouseup but I haven't had a chance to validate that. Then you wouldn't have to have your alternate flow as I did above. The other thing I put in there optionally is to get the commonAncestorContainer from the range, in case you need to look at that or its parents to see if it's contained in a certain element id or something. I needed it to validate that the selection was from the markup the user was allowed to markup.

Related