89

I'm using this function to copy a URL to the clipboard:

function CopyUrl($this){

  var querySelector = $this.next().attr("id");
  var emailLink = document.querySelector("#"+querySelector);

  var range = document.createRange();
  range.selectNode(emailLink);  
  window.getSelection().addRange(range);  

  try {  
    // Now that we've selected the anchor text, execute the copy command  
    var successful = document.execCommand('copy', false, null);
    var msg = successful ? 'successful' : 'unsuccessful'; 

    if(true){
        $this.addClass("copied").html("Copied");
    }

  } catch(err) {  
    console.log('Oops, unable to copy');  
  }  

  // Remove the selections - NOTE: Should use   
  // removeRange(range) when it is supported  
  window.getSelection().removeAllRanges();
}

Everything works fine on desktop browsers, but not on iOS devices, where my function returns successfully, but the data isn't copied to the clipboard at all. What's causing this and how could I solve this problem?

Polsonby
  • 22,825
  • 19
  • 59
  • 74
Nino Amisulashvili
  • 903
  • 1
  • 7
  • 7

14 Answers14

139

Update! iOS >= 10

Looks like with the help of selection ranges and some little hack it is possible to directly copy to the clipboard on iOS (>= 10) Safari. I personally tested this on iPhone 5C iOS 10.3.3 and iPhone 8 iOS 11.1. However, there seem to be some restrictions, which are:

  1. Text can only be copied from <input> and <textarea> elements.
  2. If the element holding the text is not inside a <form>, then it must be contenteditable.
  3. The element holding the text must not be readonly (though you may try, this is not an "official" method documented anywhere).
  4. The text inside the element must be in selection range.

To cover all four of these "requirements", you will have to:

  1. Put the text to be copied inside an <input> or <textarea> element.
  2. Save the old values of contenteditable and readonly of the element to be able to restore them after copying.
  3. Change contenteditable to true and readonly to false.
  4. Create a range to select the desired element and add it to the window's selection.
  5. Set the selection range for the entire element.
  6. Restore the previous contenteditable and readonly values.
  7. Run execCommand('copy').

This will cause the caret of the user's device to move and select all the text in the element you want, and then automatically issue the copy command. The user will see the text being selected and the tool-tip with the options select/copy/paste will be shown.

Now, this looks a little bit complicated and too much of an hassle to just issue a copy command, so I'm not sure this was an intended design choice by Apple, but who knows... in the mean time, this currently works on iOS >= 10.

With this said, polyfills like this one could be used to simplify this action and make it cross-browser compatible (thanks @Toskan for the link in the comments).

Working example

To summarize, the code you'll need looks like this:

function iosCopyToClipboard(el) {
    var oldContentEditable = el.contentEditable,
        oldReadOnly = el.readOnly,
        range = document.createRange();

    el.contentEditable = true;
    el.readOnly = false;
    range.selectNodeContents(el);

    var s = window.getSelection();
    s.removeAllRanges();
    s.addRange(range);

    el.setSelectionRange(0, 999999); // A big number, to cover anything that could be inside the element.

    el.contentEditable = oldContentEditable;
    el.readOnly = oldReadOnly;

    document.execCommand('copy');
}

Note that the el parameter to this function must be an <input> or a <textarea>.

Old answer: previous iOS versions

On iOS < 10 there are some restrictions for Safari (which actually are security measures) to the Clipboard API:

  • It fires copy events only on a valid selection and cut and paste only in focused editable fields.
  • It only supports OS clipboard reading/writing via shortcut keys, not through document.execCommand(). Note that "shorcut key" means some clickable (e.g. copy/paste action menu or custom iOS keyboard shortcut) or physical key (e.g. connected bluetooth keyboard).
  • It doesn't support the ClipboardEvent constructor.

So (at least as of now) it's not possible to programmatically copy some text/value in the clipboard on an iOS device using Javascript. Only the user can decide whether to copy something.

It is however possible to select something programmatically, so that the user only has to hit the "Copy" tool-tip shown on the selection. This can be achieved with the exact same code as above, just removing the execCommand('copy'), which is indeed not going to work.

pgsandstrom
  • 14,361
  • 13
  • 70
  • 104
Marco Bonelli
  • 63,369
  • 21
  • 118
  • 128
  • @Peege151 "shorcut key" means some clickable (e.g. normal copy/paste action menu or custom iOS keyboard shortcut) or physical key (e.g. connected bluetooth keyboard etc). Anyway something that is triggered by the user, and not programmatically. – Marco Bonelli Feb 22 '16 at 21:51
  • @MarcoBonelli, Nice Answer. I have a relevant question, while user press copy (iOS keyboard shortcut), then I need to redirect him/her to another page. How to do it? – Md Mahbubur Rahman Nov 22 '16 at 10:00
  • Programmatically nearly always means from a user triggered event like a click.. but it still doesn't work for me – Dominic Aug 21 '17 at 08:26
  • @DominicTobias if you spend two minutes reading my answer maybe you'll understand why it doesn't work. I literally say that *"it's not possible to programmatically copy [...]"*. – Marco Bonelli Aug 21 '17 at 09:39
  • +1. just a friendly suggestion, maybe put the "it doesn't work on iOS" as first statement. So one does not have to read to the bottom of it. I hope the apple boys will change that, it is crap. But seems like the clipboard is so vulnerable on iOS that it needs protection – Toskan Dec 06 '17 at 21:29
  • the people here state they _CAN_ copy to clipboard on iOS safari 10+. https://github.com/lgarron/clipboard-polyfill – Toskan Dec 07 '17 at 04:30
  • @MarcoBonelli your iOS 10+ update is interesting. Could you please explain why readonly needs to be false? Others are saying readonly needs to be true, otherwise the keyboard will pop up (and if so, would blurring the element prevent that?) Also of interest is Matthew Dean's comment about using preventDefault(). – yezzz Dec 08 '17 at 15:05
  • @yezzz I tested it and it seemed to work with it set to false, I have no "official" explanation for it. I'll add a note on the answer. – Marco Bonelli Dec 08 '17 at 15:20
  • Thanks. So you did not get a keyboard? And setting readonly to readonly/true did not work for you? – yezzz Dec 08 '17 at 17:22
  • @yezzz yep. ㅤㅤㅤㅤ – Marco Bonelli Dec 08 '17 at 17:43
  • Setting readonly to true doesn't seem to stop the keyboard from showing on copy. I was able to solve the issue by using [select](https://github.com/zenorocha/select/blob/master/src/select.js). – jchook Mar 24 '18 at 03:00
  • I think there's a couple of typos in the snippet ``` el.contenteditable = true; el.readonly = false; ``` should be ``` el.contentEditable = true; el.readOnly = false; ``` The property names are case-sensitive and should be camel-cased – jrz Sep 01 '18 at 11:49
  • I think the `el.contentEditable` flag must be a string, not a boolean (`"true"`), according to https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/contentEditable. – Danilo Bargen Oct 16 '18 at 12:07
  • @DaniloBargen yes, it is automatically converted to a string for any value you use, except `false`, which disables it. It has "special" getters/setters. – Marco Bonelli Oct 16 '18 at 12:12
  • I don't want to edit your answer since I don't know JS that well but I believe you can replace the 9999.. part in your range with `el.length` since magic numbers are generally frowned upon. I may be wrong though, don't know enough to say for certain. For example I code iOS and emoji characters are 1 character long but register as 2-length and sometimes even 4-length so this would require some testing before amending the answer. – Albert Renshaw Oct 30 '18 at 20:20
  • @AlbertRenshaw that's exactly why I have that strange 999999 in the code, because you don't know for sure how long the selction needs to be. I did not have time to test so I didn't change that. – Marco Bonelli Oct 30 '18 at 22:06
  • @Coder I found that to work on desktop chrome as well as ios required: el.focus(); el.select(); before document.execCommand("copy"); hope this helps :) – Carlene Jun 28 '19 at 14:14
  • Simply appending `input.setSelectionRange(0, input.value.length)` after `input.select()` fixed it for me. iOS 12.4.1, on an input element. – Tom Sep 11 '19 at 18:21
  • Sorry for so many questions... Been struggling to get this to work on windows. Is this exclusively for ios? Thanks. – Cymro Sep 05 '20 at 16:14
  • 1
    @Cymro yes, it's exclusive to iOS. You don't need all that stuff to do this on Windows. There are plenty of posts and answers out there that explain how to do it. – Marco Bonelli Sep 05 '20 at 16:20
49

I've searched for some solutions and I've found one that actually works: http://www.seabreezecomputers.com/tips/copy2clipboard.htm

Basically, example could be something like:

var $input = $(' some input/textarea ');
$input.val(result);
if (navigator.userAgent.match(/ipad|ipod|iphone/i)) {
  var el = $input.get(0);
  var editable = el.contentEditable;
  var readOnly = el.readOnly;
  el.contentEditable = 'true';
  el.readOnly = 'false';
  var range = document.createRange();
  range.selectNodeContents(el);
  var sel = window.getSelection();
  sel.removeAllRanges();
  sel.addRange(range);
  el.setSelectionRange(0, 999999);
  el.contentEditable = editable;
  el.readOnly = readOnly;
} else {
  $input.select();
}
document.execCommand('copy');
$input.blur();
isherwood
  • 58,414
  • 16
  • 114
  • 157
Marko Milojevic
  • 731
  • 7
  • 12
  • 4
    Works on my iOS 10 device! – Rikard Askelöf Mar 24 '17 at 13:41
  • It works on IOS 10, thanks! Just a minor detail in your example, replace "result" which is an undefined variable for the actual text you want to put in the clipboard. – David V May 24 '17 at 17:07
  • 2
    Works. But it opens the keyboard on iOS and closes it in a split second. But you can see the keyboard. – pixelscreen Jun 14 '17 at 12:04
  • 2
    Nice thanks, you can avoid the keyboard altogether by setting readOnly to true instead of false @pixelscreen – Dominic Jul 14 '17 at 08:40
  • 3
    Yea works! And I confirm @DominicTobias 's comment (set readOnly = true) works too. – Cesar Sep 01 '17 at 20:50
  • Could you provide an answer to copy text to the clipboard. i.e. add an element, set the text in the element etc. Much appreciated. – Cymro Sep 03 '20 at 09:52
48

This is my cross browser implementation (including iOS)

You can test it by running the snippet below

Example:

copyToClipboard("Hello World");

/**
 * Copy a string to clipboard
 * @param  {String} string         The string to be copied to clipboard
 * @return {Boolean}               returns a boolean correspondent to the success of the copy operation.
 * @see https://stackoverflow.com/a/53951634/938822
 */
function copyToClipboard(string) {
  let textarea;
  let result;

  try {
    textarea = document.createElement('textarea');
    textarea.setAttribute('readonly', true);
    textarea.setAttribute('contenteditable', true);
    textarea.style.position = 'fixed'; // prevent scroll from jumping to the bottom when focus is set.
    textarea.value = string;

    document.body.appendChild(textarea);

    textarea.focus();
    textarea.select();

    const range = document.createRange();
    range.selectNodeContents(textarea);

    const sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    textarea.setSelectionRange(0, textarea.value.length);
    result = document.execCommand('copy');
  } catch (err) {
    console.error(err);
    result = null;
  } finally {
    document.body.removeChild(textarea);
  }

  // manual copy fallback using prompt
  if (!result) {
    const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
    const copyHotkey = isMac ? '⌘C' : 'CTRL+C';
    result = prompt(`Press ${copyHotkey}`, string); // eslint-disable-line no-alert
    if (!result) {
      return false;
    }
  }
  return true;
}
Demo: <button onclick="copyToClipboard('It works!\nYou can upvote my answer now :)') ? this.innerText='Copied!': this.innerText='Sorry :(' ">Click here</button>

<p>
  <textarea placeholder="(Testing area) Paste here..." cols="80" rows="4"></textarea>
</p>

NOTE: It doesn't work when it is not initiated by the user, like timeouts or any async event!

It must come from a trusted event like called from a click event on a button

Vitim.us
  • 20,746
  • 15
  • 92
  • 109
25

Problem: iOS Safari only allows document.execCommand('copy') for text within a contentEditable container.

Solution: detect iOS Safari and quickly toggle contentEditable before executing document.execCommand('copy').

The function below works in all browsers. Call with a CSS Selector or HTMLElement:

function copyToClipboard(el) {

    // resolve the element
    el = (typeof el === 'string') ? document.querySelector(el) : el;

    // handle iOS as a special case
    if (navigator.userAgent.match(/ipad|ipod|iphone/i)) {

        // save current contentEditable/readOnly status
        var editable = el.contentEditable;
        var readOnly = el.readOnly;

        // convert to editable with readonly to stop iOS keyboard opening
        el.contentEditable = true;
        el.readOnly = true;

        // create a selectable range
        var range = document.createRange();
        range.selectNodeContents(el);

        // select the range
        var selection = window.getSelection();
        selection.removeAllRanges();
        selection.addRange(range);
        el.setSelectionRange(0, 999999);

        // restore contentEditable/readOnly to original state
        el.contentEditable = editable;
        el.readOnly = readOnly;
    }
    else {
        el.select();
    }

    // execute copy command
    document.execCommand('copy');
}
input { font-size: 14px; font-family: tahoma; }
button { font-size: 14px; font-family: tahoma; }
<input class="important-message" type="text" value="Hello World" />
<button onclick="copyToClipboard('.important-message')">Copy</button>
John Doherty
  • 3,669
  • 36
  • 38
  • 2
    Note: there are a few additional caveats I discovered with the above approach when using iOS 10 & 11. a) The input needs to have a sufficient width. Setting a width of zero or 1px in your CSS will not work, if you were hoping to copy an input the user can't see. (How big? Who knows?) Setting a relative position off-screen still seems to be fine. b) If you add event.preventDefault() to this, note that it will cause the keyboard input (or form navigation input?) popup to toggle, negating the effect of using `readOnly`. Hope that helps others! – Matthew Dean Nov 21 '17 at 22:06
12

Please check my solution.

It works on Safari (tested on iPhone 7 and iPad) and on other browsers.

window.Clipboard = (function(window, document, navigator) {
    var textArea,
        copy;

    function isOS() {
        return navigator.userAgent.match(/ipad|iphone/i);
    }

    function createTextArea(text) {
        textArea = document.createElement('textArea');
        textArea.value = text;
        document.body.appendChild(textArea);
    }

    function selectText() {
        var range,
            selection;

        if (isOS()) {
            range = document.createRange();
            range.selectNodeContents(textArea);
            selection = window.getSelection();
            selection.removeAllRanges();
            selection.addRange(range);
            textArea.setSelectionRange(0, 999999);
        } else {
            textArea.select();
        }
    }

    function copyToClipboard() {        
        document.execCommand('copy');
        document.body.removeChild(textArea);
    }

    copy = function(text) {
        createTextArea(text);
        selectText();
        copyToClipboard();
    };

    return {
        copy: copy
    };
})(window, document, navigator);

// How to use
Clipboard.copy('text to be copied');

https://gist.github.com/rproenca/64781c6a1329b48a455b645d361a9aa3 https://fiddle.jshell.net/k9ejqmqt/1/

Hope that helps you.

Regards.

Rodrigo
  • 639
  • 8
  • 11
5

My solution was created by combining others answers from this page.

Unlike the other answers, it does not require that you already have an element on the page. It will create its own textarea, and clean up the mess afterwards.

function copyToClipboard(str) {
    var el = document.createElement('textarea');
    el.value = str;
    el.setAttribute('readonly', '');
    el.style = {position: 'absolute', left: '-9999px'};
    document.body.appendChild(el);

    if (navigator.userAgent.match(/ipad|ipod|iphone/i)) {
        // save current contentEditable/readOnly status
        var editable = el.contentEditable;
        var readOnly = el.readOnly;

        // convert to editable with readonly to stop iOS keyboard opening
        el.contentEditable = true;
        el.readOnly = true;

        // create a selectable range
        var range = document.createRange();
        range.selectNodeContents(el);

        // select the range
        var selection = window.getSelection();
        selection.removeAllRanges();
        selection.addRange(range);
        el.setSelectionRange(0, 999999);

        // restore contentEditable/readOnly to original state
        el.contentEditable = editable;
        el.readOnly = readOnly;
    } else {
        el.select(); 
    }

    document.execCommand('copy');
    document.body.removeChild(el);
}
Eric Seastrand
  • 2,473
  • 1
  • 29
  • 36
  • @Jonah please let me know which one of the other solutions on this page *does* work for you. That way I can improve my answer to help others. – Eric Seastrand Dec 12 '18 at 13:35
  • 1
    Hey Eric, actually none of them do. I've tried everything and afict this is not possible to do on an iphone (I'm on ios 12) on safari. Please lmk if I am incorrect -- I'd love a solution -- and perhaps post a fiddle to a working solution and I'll test on my phone. – Jonah Dec 12 '18 at 14:05
  • @EricSeastrand I'm trying to implement this solution and range.selectNodeContents(el); is collapsed I think if the range is collapsed its not actually selected anything when the copy executes My el is an input type="text" with a defaultValue do you know anything about this? – gwar9 Feb 12 '19 at 00:36
  • @Jonah I am facing same issue on IOS 12 version. Did you find any solution? – KiddoDeveloper Jul 23 '19 at 05:31
  • No, iirc it wasn't possible. – Jonah Jul 23 '19 at 05:32
5

iOS 13.4 and newer

As of version 13.4, iOS Safari supports the modern async clipboard API:

Like with everything in JavaScript, the newer API is about 1000x nicer but you still need gross fallback code, since a bunch of your users will be on old versions for some years.

Here's how to use the new clipboard API with the code in the original question:

function CopyUrl($this){
  var querySelector = $this.next().attr("id");
  var emailLink = document.querySelector("#"+querySelector);

  if (navigator.clipboard) {
    var myText = emailLink.textContent;
    navigator.clipboard.writeText(myText).then(function() {
      // Do something to indicate the copy succeeded
    }).catch(function() {
      // Do something to indicate the copy failed
    });
  } else {
    // Here's where you put the fallback code for older browsers.
  }
}
nfagerlund
  • 111
  • 1
  • 3
  • This works but because I developed on a remote host, the secure connection, https, is needed to have navigator.clipboard - https://stackoverflow.com/questions/51805395/navigator-clipboard-is-undefined – shohey1226 Jun 17 '21 at 23:07
3

Clipboard API was added in Safari 13.1, see here https://webkit.org/blog/10247/new-webkit-features-in-safari-13-1/

It's now as simple as navigator.clipboard.writeText("Text to copy")

fujifish
  • 951
  • 10
  • 10
2

nice one, here's the typescript refactor of above in case anyone is interested (written as ES6 module):

type EditableInput = HTMLTextAreaElement | HTMLInputElement;

const selectText = (editableEl: EditableInput, selectionStart: number, selectionEnd: number) => {
    const isIOS = navigator.userAgent.match(/ipad|ipod|iphone/i);
    if (isIOS) {
        const range = document.createRange();
        range.selectNodeContents(editableEl);

        const selection = window.getSelection(); // current text selection
        selection.removeAllRanges();
        selection.addRange(range);
        editableEl.setSelectionRange(selectionStart, selectionEnd);
    } else {
        editableEl.select();
    }
};

const copyToClipboard = (value: string): void => {
    const el = document.createElement('textarea'); // temporary element
    el.value = value;

    el.style.position = 'absolute';
    el.style.left = '-9999px';
    el.readOnly = true; // avoid iOs keyboard opening
    el.contentEditable = 'true';

    document.body.appendChild(el);

    selectText(el, 0, value.length);

    document.execCommand('copy');
    document.body.removeChild(el);

};

export { copyToClipboard };
Kevin K.
  • 542
  • 5
  • 7
1

This one worked for me for a readonly input element.

copyText = input => {
    const isIOSDevice = navigator.userAgent.match(/ipad|iphone/i);

    if (isIOSDevice) {
        input.setSelectionRange(0, input.value.length);
    } else {
        input.select();
    }

    document.execCommand('copy');
};
  • The conditional isn't necessary, simply execute `input.setSelectionRange(0, input.value.length)` just after `input.select` as it doesn't hurt – Tom Sep 11 '19 at 18:20
1

My function for ios and other browsers copying to clipboard after tested on ios: 5c,6,7

/**
 * Copies to Clipboard value
 * @param {String} valueForClipboard value to be copied
 * @param {Boolean} isIOS is current browser is Ios (Mobile Safari)
 * @return {boolean} shows if copy has been successful
 */
const copyToClipboard = (valueForClipboard, isIOS) => {
    const textArea = document.createElement('textarea');
    textArea.value = valueForClipboard;

    textArea.style.position = 'absolute';
    textArea.style.left = '-9999px'; // to make it invisible and out of the reach
    textArea.setAttribute('readonly', ''); // without it, the native keyboard will pop up (so we show it is only for reading)

    document.body.appendChild(textArea);

    if (isIOS) {
        const range = document.createRange();
        range.selectNodeContents(textArea);

        const selection = window.getSelection();
        selection.removeAllRanges(); // remove previously selected ranges
        selection.addRange(range);
        textArea.setSelectionRange(0, valueForClipboard.length); // this line makes the selection in iOS
    } else {
        textArea.select(); // this line is for all other browsers except ios
    }

    try {
        return document.execCommand('copy'); // if copy is successful, function returns true
    } catch (e) {
        return false; // return false to show that copy unsuccessful
    } finally {
        document.body.removeChild(textArea); // delete textarea from DOM
    }
};

above answer about contenteditable=true. I think only belongs to divs. And for <textarea> is not applicable.

isIOS variable can be checked as

const isIOS = navigator.userAgent.match(/ipad|ipod|iphone/i);

Mihey Mik
  • 1,643
  • 13
  • 18
  • This solution worked best for me: works on safari desktop and safari mobile (iOS). Also, I prefer that interface because I do not have to select an input / textarea field, but just can provide the text by passing an argument. – Samuel Jun 30 '19 at 10:35
1

Update: Looks like with latest browsers you can now use the Clipboard API:

https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText

Using navigator.clipboard.writeText('MyText') will write any String you need in the clipboard, no need for inputs, document.execCommand('copy') etc...

Robin Payot
  • 111
  • 1
  • 4
0

This improves Marco's answer by allowing the text to be passed as a variable. This works on ios >10. This does not work on Windows.

function CopyToClipboardIOS(TheText) {
  var el=document.createElement('input');
  el.setAttribute('style','position:absolute;top:-9999px');
  el.value=TheText;
  document.body.appendChild(el);
  var range = document.createRange();
  el.contentEditable=true;
  el.readOnly = false;
  range.selectNodeContents(el);
  var s=window.getSelection();
  s.removeAllRanges();
  s.addRange(range);
  el.setSelectionRange(0, 999999);
  document.execCommand('copy');
  el.remove();
}
Cymro
  • 1,201
  • 1
  • 11
  • 29
-1
<input id="copyIos" type="hidden" value="">
var clipboard = new Clipboard('.copyUrl');
                //兼容ios复制
                $('.copyUrl').on('click',function() {
                    var $input = $('#copyIos');
                    $input.val(share_url);
                    if (navigator.userAgent.match(/ipad|ipod|iphone/i)) {
                        clipboard.on('success', function(e) {
                            e.clearSelection();
                            $.sDialog({
                                skin: "red",
                                content: 'copy success!',
                                okBtn: false,
                                cancelBtn: false,
                                lock: true
                            });
                            console.log('copy success!');
                        });
                    } else {
                        $input.select();
                    }
                    //document.execCommand('copy');
                    $input.blur();
                });