327

How can a web application detect a paste event and retrieve the data to be pasted?

I would like to remove HTML content before the text is pasted into a rich text editor.

Cleaning the text after being pasted afterwards works, but the problem is that all previous formatting is lost. For example, I can write a sentence in the editor and make it bold, but when I paste new text, all formatting is lost. I want to clean just the text that is pasted, and leave any previous formatting untouched.

Ideally, the solution should work across all modern browsers (e.g., MSIE, Gecko, Chrome, and Safari).

Note that MSIE has clipboardData.getData(), but I could not find similar functionality for other browsers.

Alex
  • 11,437
  • 7
  • 39
  • 51
  • All these answers explain how to get text content. Getting image content or file content requires much more work. Maybe we can change the title to "JavaScript get sanitized text clipboard data..." – 1.21 gigawatts Jan 30 '18 at 19:53
  • 2
    like nico said: `event.clipboardData.getData('Text')` worked for me. – Andre Elrico Aug 06 '19 at 15:31
  • `document.addEventListener('paste'...` worked for me but caused conflicts if a user wanted to be able to paste elsewhere on the page. Then I tried `myCanvasElement.addEventListener('paste'...`, but that didn't work. Eventually I figured out `myCanvasElement.parentElement.addEventListener('paste'...` worked. – Ryan Mar 14 '20 at 20:58

20 Answers20

342

Solution #1 (Plain Text only and requires Firefox 22+)

Works for IE6+, FF 22+, Chrome, Safari, Edge (Only tested in IE9+, but should work for lower versions)

If you need support for pasting HTML or Firefox <= 22, see Solution #2.

HTML

<div id='editableDiv' contenteditable='true'>Paste</div>

JavaScript

function handlePaste (e) {
    var clipboardData, pastedData;

    // Stop data actually being pasted into div
    e.stopPropagation();
    e.preventDefault();

    // Get pasted data via clipboard API
    clipboardData = e.clipboardData || window.clipboardData;
    pastedData = clipboardData.getData('Text');
    
    // Do whatever with pasteddata
    alert(pastedData);
}

document.getElementById('editableDiv').addEventListener('paste', handlePaste);

JSFiddle: https://jsfiddle.net/swL8ftLs/12/

Note that this solution uses the parameter 'Text' for the getData function, which is non-standard. However, it works in all browsers at the time of writing.


Solution #2 (HTML and works for Firefox <= 22)

Tested in IE6+, FF 3.5+, Chrome, Safari, Edge

HTML

<div id='div' contenteditable='true'>Paste</div>

JavaScript

var editableDiv = document.getElementById('editableDiv');

function handlepaste (e) {
    var types, pastedData, savedContent;
    
    // Browsers that support the 'text/html' type in the Clipboard API (Chrome, Firefox 22+)
    if (e && e.clipboardData && e.clipboardData.types && e.clipboardData.getData) {
            
        // Check for 'text/html' in types list. See abligh's answer below for deatils on
        // why the DOMStringList bit is needed. We cannot fall back to 'text/plain' as
        // Safari/Edge don't advertise HTML data even if it is available
        types = e.clipboardData.types;
        if (((types instanceof DOMStringList) && types.contains("text/html")) || (types.indexOf && types.indexOf('text/html') !== -1)) {
        
            // Extract data and pass it to callback
            pastedData = e.clipboardData.getData('text/html');
            processPaste(editableDiv, pastedData);

            // Stop the data from actually being pasted
            e.stopPropagation();
            e.preventDefault();
            return false;
        }
    }
    
    // Everything else: Move existing element contents to a DocumentFragment for safekeeping
    savedContent = document.createDocumentFragment();
    while(editableDiv.childNodes.length > 0) {
        savedContent.appendChild(editableDiv.childNodes[0]);
    }
    
    // Then wait for browser to paste content into it and cleanup
    waitForPastedData(editableDiv, savedContent);
    return true;
}

function waitForPastedData (elem, savedContent) {

    // If data has been processes by browser, process it
    if (elem.childNodes && elem.childNodes.length > 0) {
    
        // Retrieve pasted content via innerHTML
        // (Alternatively loop through elem.childNodes or elem.getElementsByTagName here)
        var pastedData = elem.innerHTML;
        
        // Restore saved content
        elem.innerHTML = "";
        elem.appendChild(savedContent);
        
        // Call callback
        processPaste(elem, pastedData);
    }
    
    // Else wait 20ms and try again
    else {
        setTimeout(function () {
            waitForPastedData(elem, savedContent)
        }, 20);
    }
}

function processPaste (elem, pastedData) {
    // Do whatever with gathered data;
    alert(pastedData);
    elem.focus();
}

// Modern browsers. Note: 3rd argument is required for Firefox <= 6
if (editableDiv.addEventListener) {
    editableDiv.addEventListener('paste', handlepaste, false);
}
// IE <= 8
else {
    editableDiv.attachEvent('onpaste', handlepaste);
}

JSFiddle: https://jsfiddle.net/nicoburns/wrqmuabo/23/

Explanation

The onpaste event of the div has the handlePaste function attached to it and passed a single argument: the event object for the paste event. Of particular interest to us is the clipboardData property of this event which enables clipboard access in non-ie browsers. In IE the equivalent is window.clipboardData, although this has a slightly different API.

See resources section below.


The handlepaste function:

This function has two branches.

The first checks for the existence of event.clipboardData and checks whether it's types property contains 'text/html' (types may be either a DOMStringList which is checked using the contains method, or a string which is checked using the indexOf method). If all of these conditions are fulfilled, then we proceed as in solution #1, except with 'text/html' instead of 'text/plain'. This currently works in Chrome and Firefox 22+.

If this method is not supported (all other browsers), then we

  1. Save the element's contents to a DocumentFragment
  2. Empty the element
  3. Call the waitForPastedData function

The waitforpastedata function:

This function first polls for the pasted data (once per 20ms), which is necessary because it doesn't appear straight away. When the data has appeared it:

  1. Saves the innerHTML of the editable div (which is now the pasted data) to a variable
  2. Restores the content saved in the DocumentFragment
  3. Calls the 'processPaste' function with the retrieved data

The processpaste function:

Does arbitrary things with the pasted data. In this case we just alert the data, you can do whatever you like. You will probably want to run the pasted data through some kind of data sanitising process.


Saving and restoring the cursor position

In a real sitution you would probably want to save the selection before, and restore it afterwards (Set cursor position on contentEditable <div>). You could then insert the pasted data at the position the cursor was in when the user initiated the paste action.

Resources:

Thanks to Tim Down to suggesting the use of a DocumentFragment, and abligh for catching an error in Firefox due to the use of DOMStringList instead of a string for clipboardData.types

Community
  • 1
  • 1
Nico Burns
  • 15,663
  • 9
  • 36
  • 51
  • 4
    Interesting. I thought I'd tried this in the past and it hadn't worked in some browser, but I'm sure you're right. I would definitely prefer moving the existing content into a `DocumentFragment` rather than using `innerHTML` for several reasons: first, you keep any existing event handlers; second, saving and restoring `innerHTML` is not guaranteed to create an identical copy of the previous DOM; third, you can then save the selection as a `Range` rather than having to faff around with adding marker elements or calculating text offsets (which is what you'd have to do if you used `innerHTML`). – Tim Down Jul 24 '11 at 10:27
  • Ah, well that explains why my event handlers were disappearing! I might just add them back afterwards though, because extracting to a `documentFragment` is a pain in IE. – Nico Burns Jul 24 '11 at 15:22
  • Another problem with this approach is that the brief time where there is no content will be visible to user as a brief flash (haven't tested this but I'd be surprised if it wasn't true). – Tim Down Jul 25 '11 at 01:14
  • 3
    There is indeed a flash of no content (FONC?), which will obviously be worse if the processing of the pasted content takes some time. Btw, why is extracting to a `DocumentFragment` a pain in IE? It's the same as in other browsers, unless you use a Range and `extractContents()` to do it, which is no more concise than the alternative in any case. I've implemented an example of your technique, using Rangy to keep things nice and uniform across browsers: http://jsfiddle.net/bQeWC/4/. – Tim Down Jul 25 '11 at 12:15
  • Yes, there is a flash of no content, one workaround I have considered is overlaying a copy of the editable area's content on top of the original area while the processing is going on. I haven't tested this yet though. Extracting a `DocumentFragment` was only really a pain because I'd never done it before, I'll change to that method now. Your Rangy library is really very impressive by the way. – Nico Burns Jul 25 '11 at 14:45
  • Do you have a newer version of this code using DocumentFragment? The code works perfect in Chrome but IE and FF get pretty ugly FONCs. – Martin Aug 30 '11 at 21:53
  • 1
    @Martin: The jsFiddle demo I posted in the comments may help. – Tim Down Sep 02 '11 at 13:29
  • Tim Down, i'd like to take the pasted data, put it into a div, get $(cleanUpDiv).text(); again and paste this instead, so i have no formatting. But somehow, i don't get it. Any help? – xotix Mar 08 '12 at 10:26
  • @xotix: change the line `pasteddata = elem.innerHTML;` to `pasteddata = $(elem).text();`. – Nico Burns Mar 10 '12 at 14:06
  • 1
    It seems that it doesn't work anymore on Firefox 28 (at least) for Windows. It never goes out of the `waitforpastedata` function – Oliboy50 Apr 15 '14 at 16:41
  • JavsScript should be JavaScript – TomWan Jan 19 '15 at 10:35
  • One limitation of this solution is that OSX Safari (as of 8.0.4) does not expose data as `text/html` to the Clipboard API. So far it seems like the only option if you need to support Safari users is to use the hacky pastebin solution. – Ben Regenspan Apr 28 '15 at 19:25
  • @Oliboy50 see my answer on how to fix Firefox (and Safari for plain text mode) – abligh Jul 08 '15 at 07:49
  • e.clipboardData is not available on IE (Internet Explorer). I checked on IE11. – Champ Jan 06 '16 at 00:50
  • @TimDown @abligh I have updated this answer to use `DocumentFragment` instead of `innerHTML` and to take account of Firefox/Safari using `DOMStringList`. It also turns out that Clipboard API events can now be used reliably cross-browser for the simple plain text case. – Nico Burns Jun 08 '16 at 21:45
  • And what are "standard" argument types and variations for clipboardData.getData()? Is there a definitive list somewhere? – jayarjo Aug 06 '16 at 07:37
  • on the first example, what can I replace // Do whatever with pasteddata in order to paste the data after I modified it? – TSR Apr 16 '17 at 15:36
  • @TSR You probably want to use something like http://stackoverflow.com/questions/2920150/insert-text-at-cursor-in-a-content-editable-div to insert a string at the cursor position. – Nico Burns Apr 17 '17 at 12:58
  • The value of the div's `id` attribute in Solution #2 should be `editableDiv` and not `id`. There is also a typo in the first comment in the function `waitForPastedData`. – Krisztián Balla Jul 14 '17 at 12:48
  • @NicoBurns How can I pasted the content somewhere else besides as an alert? I would like to validate the content using a validation expression. Is there any way to paste the data back inside the textbox but not allow it to be editable after? This way I would still be able to paste large data fast without having my users being able to edit after pasting. – justinpees Aug 26 '17 at 20:55
  • @JennyO'Reilly It's on my todo list to update this answer to use events rather than timeouts like user669677's answer above. But this solution should work, as you remove the existing content from the `editableDiv`, then you wait until the *newly pasted data* appears, which will trigger the if condition. – Nico Burns Nov 03 '17 at 15:26
  • I think there is a slight misunderstanding. The order is as follows: 1. The existing content is removed *before* the pasted data appears in the div (and saved for later). 2. We wait until the pasted data appears in the div. 3. Then we remove (and save) the pasted data, and replace it with the old content we saved before. 4. We can at this point do what we like with the pasted data. In the example we alert() it. – Nico Burns Nov 06 '17 at 15:03
  • 1
    FYI: Edge now supports reading data with MIME-Type `text/html` using the W3C Clipboard API. In the past such an attempt would throw an exception. So one no longer needs this workaround/hack for Edge. – Krisztián Balla Jan 11 '18 at 13:25
159

The situation has changed since writing this answer: now that Firefox has added support in version 22, all major browsers now support accessing the clipboard data in a paste event. See Nico Burns's answer for an example.

In the past this was not generally possible in a cross-browser way. The ideal would be to be able to get the pasted content via the paste event, which is possible in recent browsers but not in some older browsers (in particular, Firefox < 22).

When you need to support older browsers, what you can do is quite involved and a bit of a hack that will work in Firefox 2+, IE 5.5+ and WebKit browsers such as Safari or Chrome. Recent versions of both TinyMCE and CKEditor use this technique:

  1. Detect a ctrl-v / shift-ins event using a keypress event handler
  2. In that handler, save the current user selection, add a textarea element off-screen (say at left -1000px) to the document, turn designMode off and call focus() on the textarea, thus moving the caret and effectively redirecting the paste
  3. Set a very brief timer (say 1 millisecond) in the event handler to call another function that stores the textarea value, removes the textarea from the document, turns designMode back on, restores the user selection and pastes the text in.

Note that this will only work for keyboard paste events and not pastes from the context or edit menus. By the time the paste event fires, it's too late to redirect the caret into the textarea (in some browsers, at least).

In the unlikely event that you need to support Firefox 2, note that you'll need to place the textarea in the parent document rather than the WYSIWYG editor iframe's document in that browser.

Community
  • 1
  • 1
Tim Down
  • 292,637
  • 67
  • 429
  • 506
  • 1
    Wow, thanks for that! Seems to be a very sophisticated hack though ;-) Could you please describe that designMode and selection thing a little more, especially in step 3? Thanks a lot! – Alex Feb 01 '10 at 14:07
  • 5
    I had a horrible feeling you'd ask that. As I say, it's quite involved: I'd suggest looking at the source of TinyMCE or CKEditor, since I haven't got the time to outline all the issues involved. Briefly though, `designMode` is a Boolean property of `document` and makes the whole page editable when `true`. WYSIWYG editors usually use an iframe with `designMode` on as the editable pane. Saving and restoring the user selection is done one way in IE and another in other browsers, as is pasting the content into the editor. You need to obtain a `TextRange` in IE and a `Range` in other browsers. – Tim Down Feb 01 '10 at 14:19
  • Thanks. I have already worked with TextRanges earlier, so this is not completely new to me. But as you mentioned, the best would be to study CKEditor (remember it as FCKeditor) and/or tinyMCE now. – Alex Feb 01 '10 at 14:23
  • I have this working in my own niche editor project. CKEditor and TinyMCE both have this working in their current versions. – Tim Down May 03 '10 at 15:38
  • 6
    @Samuel: You can detect it using the `paste` event but it's generally too late by then to redirect the paste into another element, so this hack won't work. The fallback in most editors is to show a dialog for the user to paste into. – Tim Down Jun 28 '11 at 22:10
  • @Tim, I've been hacking on this today. I would like to wrap the selection immediately before paste so that I can clean up the inner HTML using rangy (thanks for that! great stuff). Do you think that's possible? – Samuel Cole Jun 28 '11 at 22:48
  • @Samuel: I'm not sure. I seem to remember experimenting with this and trying and failing to get the pasted content to go inside an element I inserted in a paste event handler, but that was years ago. My gut feeling is that it won't be viable, but I'd be interested to know. – Tim Down Jun 29 '11 at 08:52
  • 6
    Some more info on this: Firefox won't allow you to move focus to another element in the `paste` event, however it will allow you to clear the contents of the element (and save it to a variable so you can restore it later). If this container is a `div` (it probably works for an `iframe` too) then you can then cycle through the pasted content using normal dom methods, or get it as a string using `innerHTML`. You can then restore the previous contents of the `div`, and insert whatever content you like. Oh, and you have to use the same timer hack as above. I'm surprised TinyMCE doesn't do this... – Nico Burns Jul 24 '11 at 00:08
  • I have added an answer demonstrating basic functionality below. It works in every browser I tested, which is most of them. It doesn't include any filtering for the pasted data, but it should be fairly easy to do that yourself from the base provided. – Nico Burns Jul 24 '11 at 03:34
  • Has anyone implemented steps 2 and 3 as a single function? Something like `getClipboardData` that can simply be called when you detect a `Ctrl-V` or a `Shift-Insert` event. – Aadit M Shah Aug 16 '12 at 15:26
  • @AaditMShah: The timer is essential so you'll always need a callback. – Tim Down Aug 16 '12 at 15:43
  • To stress: The FF apparently locks out .focus() moves inside onpaste, even BEFORE the paste itself is performed. Thus it is necessary to wrap the onpaste with a keystroke interceptor. – DragonLord Oct 18 '12 at 21:44
  • Hi, can anyone of you tell me exactly the browser support of the native JS `paste` event? What minimum versions of different browsers support it? – SexyBeast Jan 11 '13 at 22:29
  • @Cupidvogel: IE since (I think) 5 or 5.5, Firefox since 3.0, Safari since 4.0 (possibly earlier), all versions of Chrome. Last time I checked no version of Opera supported it, although I think I've seen something that suggests it's coming soon. – Tim Down Jan 12 '13 at 12:09
  • I just wanted to add that this ISN'T A HACK! By that type of guideline building any User Generated Content oriented application in the browser would be a hack. HTML with JavaScript just wasn't intended for any of this type of development from the onset, but it's what we use it for today as we wait for the standards and APIs to migrate more and more in that direction. So we should probably stop referring to these sorts of approaches as hacks. Jus' sayin'! :) – Resist Design Feb 01 '13 at 14:38
  • 8
    @ResistDesign: I disagree - it's an inelegant and complicated way to make up for the lack of a sensible API. It would be better to be able to get the pasted content directly from the paste event, which is [possible in a limited way in some browsers](http://codebits.glennjones.net/editing/getclipboarddata.htm). – Tim Down Feb 01 '13 at 15:03
  • I totally agree with you Tim, but what percentage of what we all do in the browser fits that description? That kind of thing seems like just another day at the office in browser land. So when hacks become the norm, are they still hacks? – Resist Design Feb 04 '13 at 13:23
  • 1
    @ResistDesign: I'd say so, but I have a strong idealist streak :) – Tim Down Feb 04 '13 at 14:57
138

Simple version:

document.querySelector('[contenteditable]').addEventListener('paste', (e) => {
    e.preventDefault();
    const text = (e.originalEvent || e).clipboardData.getData('text/plain');
    window.document.execCommand('insertText', false, text);
});

Using clipboardData

Demo : http://jsbin.com/nozifexasu/edit?js,output

Edge, Firefox, Chrome, Safari, Opera tested.

Document.execCommand() is obsolete now.


Note: Remember to check input/output at server-side also (like PHP strip-tags)

SergeyR
  • 386
  • 3
  • 10
l2aelba
  • 18,585
  • 20
  • 90
  • 131
  • 4
    This works really well, but no version of IE allows access to clipboardData from the event :( Great solution, though, this should be higher! – Eric Wood Dec 27 '13 at 21:04
  • 1
    It looks like you could get to the clipboard data in IE a different way though, so if you detect IE you could use that data instead of the prompt fallback: http://msdn.microsoft.com/en-us/library/ie/ms535220(v=vs.85).aspx – Andrew Feb 28 '14 at 19:29
  • 4
    best cross browser answer found so far. just add the code for IE and its perfect. – Arturo May 22 '14 at 23:15
  • In IE it is not possible to get the "html" version of what was pasted to the clipboard (possible formats are [Text and URL](http://msdn.microsoft.com/en-us/library/ie/ms536436(v=vs.85).aspx)). The text version is ok if you don't want any html, but if you want to clean it up (for example , paste from word and remove all the styles with jQueryClean) you won't be able to do it this way. – Rui Jun 30 '14 at 14:00
  • 1
    prompting would work only for text, whilst pasting can contain /anything/. – Lodewijk Sep 10 '14 at 15:36
  • Fantastic answer – thumps up! Out of interest: could you please explain the `(e.originalEvent || e)` part? – Gregor Sep 24 '14 at 17:13
  • 6
    This works in IE (ah, sweet, contrary IE) `window.clipboardData.getData('Text');` – Benjineer Mar 31 '15 at 00:49
  • 9
    ```e.preventDefault(); if (e.clipboardData) { content = (e.originalEvent || e).clipboardData.getData('text/plain'); document.execCommand('insertText', false, content); } else if (window.clipboardData) { content = window.clipboardData.getData('Text'); document.selection.createRange().pasteHTML(content); }``` – Yukulelix Jul 31 '15 at 23:03
  • @Yukulelix seems to work in IE thanks. Too bad Edge does not support clipboard api yet: https://dev.windows.com/en-us/microsoft-edge/platform/status/clipboardapi – JP Hellemons Mar 09 '16 at 08:53
30

Live Demo

Tested on Chrome / FF / IE11

There is a Chrome/IE annoyance which is that these browsers add <div> element for each new line. There is a post about this here and it can be fixed by setting the contenteditable element to be display:inline-block

Select some highlighted HTML and paste it here:

function onPaste(e){
  var content;
  e.preventDefault();

  if( e.clipboardData ){
    content = e.clipboardData.getData('text/plain');
    document.execCommand('insertText', false, content);
    return false;
  }
  else if( window.clipboardData ){
    content = window.clipboardData.getData('Text');
    if (window.getSelection)
      window.getSelection().getRangeAt(0).insertNode( document.createTextNode(content) );
  }
}


/////// EVENT BINDING /////////
document.querySelector('[contenteditable]').addEventListener('paste', onPaste);
[contenteditable]{ 
  /* chroem bug: https://stackoverflow.com/a/24689420/104380 */
  display:inline-block;
  width: calc(100% - 40px);
  min-height:120px; 
  margin:10px;
  padding:10px;
  border:1px dashed green;
}

/* 
 mark HTML inside the "contenteditable"  
 (Shouldn't be any OFC!)'
*/
[contenteditable] *{
  background-color:red;
}
<div contenteditable></div>
vsync
  • 87,559
  • 45
  • 247
  • 317
  • 1
    I needed a paste as plain text feature. Tested on IE9 and IE10 and works great. Needless to mention that works on major browsers as well... Thanks. – Savas Vedova Jun 22 '15 at 22:09
  • 2
    Your code contains a bug: if(e.originalEvent.clipboardData) can cause a NPE as you don't know if e.originalEvent exists at that point – Sebastian Mar 03 '16 at 13:51
15

I've written a little proof of concept for Tim Downs proposal here with off-screen textarea. And here goes the code:

<html>
<head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"></script> 
<script language="JavaScript">
 $(document).ready(function()
{

var ctrlDown = false;
var ctrlKey = 17, vKey = 86, cKey = 67;

$(document).keydown(function(e)
{
    if (e.keyCode == ctrlKey) ctrlDown = true;
}).keyup(function(e)
{
    if (e.keyCode == ctrlKey) ctrlDown = false;
});

$(".capture-paste").keydown(function(e)
{
    if (ctrlDown && (e.keyCode == vKey || e.keyCode == cKey)){
        $("#area").css("display","block");
        $("#area").focus();         
    }
});

$(".capture-paste").keyup(function(e)
{
    if (ctrlDown && (e.keyCode == vKey || e.keyCode == cKey)){                      
        $("#area").blur();
        //do your sanitation check or whatever stuff here
        $("#paste-output").text($("#area").val());
        $("#area").val("");
        $("#area").css("display","none");
    }
});

});
</script>

</head>
<body class="capture-paste">

<div id="paste-output"></div>


    <div>
    <textarea id="area" style="display: none; position: absolute; left: -99em;"></textarea>
    </div>

</body>
</html>

Just copy and paste the whole code into one html file and try to paste (using ctrl-v) text from clipboard anywhere on the document.

I've tested it in IE9 and new versions of Firefox, Chrome and Opera. Works quite well. Also it's good that one can use whatever key combination he prefers to triger this functionality. Of course don't forget to include jQuery sources.

Feel free to use this code and if you come with some improvements or problems please post them back. Also note that I'm no Javascript developer so I may have missed something (=>do your own testign).

JanM
  • 1,137
  • 1
  • 11
  • 19
  • Macs don't paste with ctrl-v, they use cmd-v. So set ctrlKey = 91 instead of 17 – Jeremy T Jan 17 '13 at 18:01
  • 2
    Or maybe it's not always 91: http://stackoverflow.com/questions/3834175/jquery-key-code-for-command-key Regardless, I'm pretty sure jQuery handles all that for you, just check e.ctrlKey or e.metaKey I think. – Jeremy T Jan 17 '13 at 18:02
  • 3
    e.ctrlKey or e.metaKey is part of the JavaScript DOM, not jQuery: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent – rvighne Oct 06 '13 at 17:29
  • 2
    I don't think this works for right clicking and pasting. A lot of people take that approach. – Eric Wood Dec 30 '13 at 21:06
10

This one does not use any setTimeout().

I have used this great article to achieve cross browser support.

$(document).on("focus", "input[type=text],textarea", function (e) {
    var t = e.target;
    if (!$(t).data("EventListenerSet")) {
        //get length of field before paste
        var keyup = function () {
            $(this).data("lastLength", $(this).val().length);
        };
        $(t).data("lastLength", $(t).val().length);
        //catch paste event
        var paste = function () {
            $(this).data("paste", 1);//Opera 11.11+
        };
        //process modified data, if paste occured
        var func = function () {
            if ($(this).data("paste")) {
                alert(this.value.substr($(this).data("lastLength")));
                $(this).data("paste", 0);
                this.value = this.value.substr(0, $(this).data("lastLength"));
                $(t).data("lastLength", $(t).val().length);
            }
        };
        if (window.addEventListener) {
            t.addEventListener('keyup', keyup, false);
            t.addEventListener('paste', paste, false);
            t.addEventListener('input', func, false);
        }
        else {//IE
            t.attachEvent('onkeyup', function () {
                keyup.call(t);
            });
            t.attachEvent('onpaste', function () {
                paste.call(t);
            });
            t.attachEvent('onpropertychange', function () {
                func.call(t);
            });
        }
        $(t).data("EventListenerSet", 1);
    }
}); 

This code is extended with selection handle before paste: demo

AsgarAli
  • 2,139
  • 1
  • 17
  • 32
10

Based on l2aelba anwser. This was tested on FF, Safari, Chrome, IE (8,9,10 and 11)

    $("#editText").on("paste", function (e) {
        e.preventDefault();

        var text;
        var clp = (e.originalEvent || e).clipboardData;
        if (clp === undefined || clp === null) {
            text = window.clipboardData.getData("text") || "";
            if (text !== "") {
                if (window.getSelection) {
                    var newNode = document.createElement("span");
                    newNode.innerHTML = text;
                    window.getSelection().getRangeAt(0).insertNode(newNode);
                } else {
                    document.selection.createRange().pasteHTML(text);
                }
            }
        } else {
            text = clp.getData('text/plain') || "";
            if (text !== "") {
                document.execCommand('insertText', false, text);
            }
        }
    });
tmorell
  • 540
  • 5
  • 14
5

For cleaning the pasted text and replacing the currently selected text with the pasted text the matter is pretty trivial:

<div id='div' contenteditable='true' onpaste='handlepaste(this, event)'>Paste</div>

JS:

function handlepaste(el, e) {
  document.execCommand('insertText', false, e.clipboardData.getData('text/plain'));
  e.preventDefault();
}
Matt Crinklaw-Vogt
  • 10,580
  • 5
  • 39
  • 44
5

This should work on all browsers that support the onpaste event and the mutation observer.

This solution goes a step beyond getting the text only, it actually allows you to edit the pasted content before it get pasted into an element.

It works by using contenteditable, onpaste event (supported by all major browsers) en mutation observers (supported by Chrome, Firefox and IE11+)

step 1

Create a HTML-element with contenteditable

<div contenteditable="true" id="target_paste_element"></div>

step 2

In your Javascript code add the following event

document.getElementById("target_paste_element").addEventListener("paste", pasteEventVerifierEditor.bind(window, pasteCallBack), false);

We need to bind pasteCallBack, since the mutation observer will be called asynchronously.

step 3

Add the following function to your code

function pasteEventVerifierEditor(callback, e)
{
   //is fired on a paste event. 
    //pastes content into another contenteditable div, mutation observer observes this, content get pasted, dom tree is copied and can be referenced through call back.
    //create temp div
    //save the caret position.
    savedCaret = saveSelection(document.getElementById("target_paste_element"));

    var tempDiv = document.createElement("div");
    tempDiv.id = "id_tempDiv_paste_editor";
    //tempDiv.style.display = "none";
    document.body.appendChild(tempDiv);
    tempDiv.contentEditable = "true";

    tempDiv.focus();

    //we have to wait for the change to occur.
    //attach a mutation observer
    if (window['MutationObserver'])
    {
        //this is new functionality
        //observer is present in firefox/chrome and IE11
        // select the target node
        // create an observer instance
        tempDiv.observer = new MutationObserver(pasteMutationObserver.bind(window, callback));
        // configuration of the observer:
        var config = { attributes: false, childList: true, characterData: true, subtree: true };

        // pass in the target node, as well as the observer options
        tempDiv.observer.observe(tempDiv, config);

    }   

}



function pasteMutationObserver(callback)
{

    document.getElementById("id_tempDiv_paste_editor").observer.disconnect();
    delete document.getElementById("id_tempDiv_paste_editor").observer;

    if (callback)
    {
        //return the copied dom tree to the supplied callback.
        //copy to avoid closures.
        callback.apply(document.getElementById("id_tempDiv_paste_editor").cloneNode(true));
    }
    document.body.removeChild(document.getElementById("id_tempDiv_paste_editor"));

}

function pasteCallBack()
{
    //paste the content into the element.
    restoreSelection(document.getElementById("target_paste_element"), savedCaret);
    delete savedCaret;

    pasteHtmlAtCaret(this.innerHTML, false, true);
}   


saveSelection = function(containerEl) {
if (containerEl == document.activeElement)
{
    var range = window.getSelection().getRangeAt(0);
    var preSelectionRange = range.cloneRange();
    preSelectionRange.selectNodeContents(containerEl);
    preSelectionRange.setEnd(range.startContainer, range.startOffset);
    var start = preSelectionRange.toString().length;

    return {
        start: start,
        end: start + range.toString().length
    };
}
};

restoreSelection = function(containerEl, savedSel) {
    containerEl.focus();
    var charIndex = 0, range = document.createRange();
    range.setStart(containerEl, 0);
    range.collapse(true);
    var nodeStack = [containerEl], node, foundStart = false, stop = false;

    while (!stop && (node = nodeStack.pop())) {
        if (node.nodeType == 3) {
            var nextCharIndex = charIndex + node.length;
            if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {
                range.setStart(node, savedSel.start - charIndex);
                foundStart = true;
            }
            if (foundStart && savedSel.end >= charIndex && savedSel.end <= nextCharIndex) {
                range.setEnd(node, savedSel.end - charIndex);
                stop = true;
            }
            charIndex = nextCharIndex;
        } else {
            var i = node.childNodes.length;
            while (i--) {
                nodeStack.push(node.childNodes[i]);
            }
        }
    }

    var sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);
}

function pasteHtmlAtCaret(html, returnInNode, selectPastedContent) {
//function written by Tim Down

var sel, range;
if (window.getSelection) {
    // IE9 and non-IE
    sel = window.getSelection();
    if (sel.getRangeAt && sel.rangeCount) {
        range = sel.getRangeAt(0);
        range.deleteContents();

        // Range.createContextualFragment() would be useful here but is
        // only relatively recently standardized and is not supported in
        // some browsers (IE9, for one)
        var el = document.createElement("div");
        el.innerHTML = html;
        var frag = document.createDocumentFragment(), node, lastNode;
        while ( (node = el.firstChild) ) {
            lastNode = frag.appendChild(node);
        }
        var firstNode = frag.firstChild;
        range.insertNode(frag);

        // Preserve the selection
        if (lastNode) {
            range = range.cloneRange();
            if (returnInNode)
            {
                range.setStart(lastNode, 0); //this part is edited, set caret inside pasted node.
            }
            else
            {
                range.setStartAfter(lastNode); 
            }
            if (selectPastedContent) {
                range.setStartBefore(firstNode);
            } else {
                range.collapse(true);
            }
            sel.removeAllRanges();
            sel.addRange(range);
        }
    }
} else if ( (sel = document.selection) && sel.type != "Control") {
    // IE < 9
    var originalRange = sel.createRange();
    originalRange.collapse(true);
    sel.createRange().pasteHTML(html);
    if (selectPastedContent) {
        range = sel.createRange();
        range.setEndPoint("StartToStart", originalRange);
        range.select();
    }
}
}

What the code does:

  1. Somebody fires the paste event by using ctrl-v, contextmenu or other means
  2. In the paste event a new element with contenteditable is created (an element with contenteditable has elevated privileges)
  3. The caret position of the target element is saved.
  4. The focus is set to the new element
  5. The content gets pasted into the new element and is rendered in the DOM.
  6. The mutation observer catches this (it registers all changes to the dom tree and content). Then fires the mutation event.
  7. The dom of the pasted content gets cloned into a variable and returned to the callback. The temporary element is destroyed.
  8. The callback receives the cloned DOM. The caret is restored. You can edit this before you append it to your target. element. In this example I'm using Tim Downs functions for saving/restoring the caret and pasting HTML into the element.

Example

document.getElementById("target_paste_element").addEventListener("paste", pasteEventVerifierEditor.bind(window, pasteCallBack), false);


function pasteEventVerifierEditor(callback, e) {
  //is fired on a paste event. 
  //pastes content into another contenteditable div, mutation observer observes this, content get pasted, dom tree is copied and can be referenced through call back.
  //create temp div
  //save the caret position.
  savedCaret = saveSelection(document.getElementById("target_paste_element"));

  var tempDiv = document.createElement("div");
  tempDiv.id = "id_tempDiv_paste_editor";
  //tempDiv.style.display = "none";
  document.body.appendChild(tempDiv);
  tempDiv.contentEditable = "true";

  tempDiv.focus();

  //we have to wait for the change to occur.
  //attach a mutation observer
  if (window['MutationObserver']) {
    //this is new functionality
    //observer is present in firefox/chrome and IE11
    // select the target node
    // create an observer instance
    tempDiv.observer = new MutationObserver(pasteMutationObserver.bind(window, callback));
    // configuration of the observer:
    var config = {
      attributes: false,
      childList: true,
      characterData: true,
      subtree: true
    };

    // pass in the target node, as well as the observer options
    tempDiv.observer.observe(tempDiv, config);

  }

}



function pasteMutationObserver(callback) {

  document.getElementById("id_tempDiv_paste_editor").observer.disconnect();
  delete document.getElementById("id_tempDiv_paste_editor").observer;

  if (callback) {
    //return the copied dom tree to the supplied callback.
    //copy to avoid closures.
    callback.apply(document.getElementById("id_tempDiv_paste_editor").cloneNode(true));
  }
  document.body.removeChild(document.getElementById("id_tempDiv_paste_editor"));

}

function pasteCallBack() {
  //paste the content into the element.
  restoreSelection(document.getElementById("target_paste_element"), savedCaret);
  delete savedCaret;

  //edit the copied content by slicing
  pasteHtmlAtCaret(this.innerHTML.slice(3), false, true);
}


saveSelection = function(containerEl) {
  if (containerEl == document.activeElement) {
    var range = window.getSelection().getRangeAt(0);
    var preSelectionRange = range.cloneRange();
    preSelectionRange.selectNodeContents(containerEl);
    preSelectionRange.setEnd(range.startContainer, range.startOffset);
    var start = preSelectionRange.toString().length;

    return {
      start: start,
      end: start + range.toString().length
    };
  }
};

restoreSelection = function(containerEl, savedSel) {
  containerEl.focus();
  var charIndex = 0,
    range = document.createRange();
  range.setStart(containerEl, 0);
  range.collapse(true);
  var nodeStack = [containerEl],
    node, foundStart = false,
    stop = false;

  while (!stop && (node = nodeStack.pop())) {
    if (node.nodeType == 3) {
      var nextCharIndex = charIndex + node.length;
      if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {
        range.setStart(node, savedSel.start - charIndex);
        foundStart = true;
      }
      if (foundStart && savedSel.end >= charIndex && savedSel.end <= nextCharIndex) {
        range.setEnd(node, savedSel.end - charIndex);
        stop = true;
      }
      charIndex = nextCharIndex;
    } else {
      var i = node.childNodes.length;
      while (i--) {
        nodeStack.push(node.childNodes[i]);
      }
    }
  }

  var sel = window.getSelection();
  sel.removeAllRanges();
  sel.addRange(range);
}

function pasteHtmlAtCaret(html, returnInNode, selectPastedContent) {
  //function written by Tim Down

  var sel, range;
  if (window.getSelection) {
    // IE9 and non-IE
    sel = window.getSelection();
    if (sel.getRangeAt && sel.rangeCount) {
      range = sel.getRangeAt(0);
      range.deleteContents();

      // Range.createContextualFragment() would be useful here but is
      // only relatively recently standardized and is not supported in
      // some browsers (IE9, for one)
      var el = document.createElement("div");
      el.innerHTML = html;
      var frag = document.createDocumentFragment(),
        node, lastNode;
      while ((node = el.firstChild)) {
        lastNode = frag.appendChild(node);
      }
      var firstNode = frag.firstChild;
      range.insertNode(frag);

      // Preserve the selection
      if (lastNode) {
        range = range.cloneRange();
        if (returnInNode) {
          range.setStart(lastNode, 0); //this part is edited, set caret inside pasted node.
        } else {
          range.setStartAfter(lastNode);
        }
        if (selectPastedContent) {
          range.setStartBefore(firstNode);
        } else {
          range.collapse(true);
        }
        sel.removeAllRanges();
        sel.addRange(range);
      }
    }
  } else if ((sel = document.selection) && sel.type != "Control") {
    // IE < 9
    var originalRange = sel.createRange();
    originalRange.collapse(true);
    sel.createRange().pasteHTML(html);
    if (selectPastedContent) {
      range = sel.createRange();
      range.setEndPoint("StartToStart", originalRange);
      range.select();
    }
  }
}
div {
  border: 1px solid black;
  height: 50px;
  padding: 5px;
}
<div contenteditable="true" id="target_paste_element"></div>

Many thanks to Tim Down See this post for the answer:

Get the pasted content on document on paste event

Mouser
  • 12,807
  • 3
  • 25
  • 51
4

Solution that works for me is adding event listener to paste event if you are pasting to a text input. Since paste event happens before text in input changes, inside my on paste handler I create a deferred function inside which I check for changes in my input box that happened on paste:

onPaste: function() {
    var oThis = this;
    setTimeout(function() { // Defer until onPaste() is done
        console.log('paste', oThis.input.value);
        // Manipulate pasted input
    }, 1);
}
Lex
  • 2,428
  • 18
  • 34
  • 2
    Horror, unfortunately, is a part of our job description ;) But I agree, this is a hack and hacks should be used ONLY when all other options are exhausted. – Lex Feb 26 '14 at 23:38
4

This was too long for a comment on Nico's answer, which I don't think works on Firefox any more (per the comments), and didn't work for me on Safari as is.

Firstly, you now appear to be able to read directly from the clipboard. Rather than code like:

if (/text\/plain/.test(e.clipboardData.types)) {
    // shouldn't this be writing to elem.value for text/plain anyway?
    elem.innerHTML = e.clipboardData.getData('text/plain');
}

use:

types = e.clipboardData.types;
if (((types instanceof DOMStringList) && types.contains("text/plain")) ||
    (/text\/plain/.test(types))) {
    // shouldn't this be writing to elem.value for text/plain anyway?
    elem.innerHTML = e.clipboardData.getData('text/plain');
}

because Firefox has a types field which is a DOMStringList which does not implement test.

Next Firefox will not allow paste unless the focus is in a contenteditable=true field.

Finally, Firefox will not allow paste reliably unless the focus is in a textarea (or perhaps input) which is not only contenteditable=true but also:

  • not display:none
  • not visibility:hidden
  • not zero sized

I was trying to hide the text field so I could make paste work over a JS VNC emulator (i.e. it was going to a remote client and there was no actually textarea etc to paste into). I found trying to hide the text field in the above gave symptoms where it worked sometimes, but typically failed on the second paste (or when the field was cleared to prevent pasting the same data twice) as the field lost focus and would not properly regain it despite focus(). The solution I came up with was to put it at z-order: -1000, make it display:none, make it as 1px by 1px, and set all the colours to transparent. Yuck.

On Safari, you the second part of the above applies, i.e. you need to have a textarea which is not display:none.

abligh
  • 23,144
  • 3
  • 41
  • 81
  • Maybe developers that work on browser rendering engines should have a page or space on documentation sites that they can use to write in notes about the features they work on. For example, if they worked on the paste function they would add, "Paste will not work if display is none, visibility is hidden or size is zero". – 1.21 gigawatts Jan 30 '18 at 19:50
3

First that comes to mind is the pastehandler of google's closure lib http://closure-library.googlecode.com/svn/trunk/closure/goog/demos/pastehandler.html

tDo
  • 518
  • 3
  • 5
  • this one seems to safely detect a paste event, but it seems not to be able to catch/return the pasted content? – Alex Feb 01 '10 at 13:39
  • @Alex: you're correct, and this also only works with textareas, not rich text editors. – Tim Down Feb 01 '10 at 13:57
3

Simple solution:

document.onpaste = function(e) {
    var pasted = e.clipboardData.getData('Text');
    console.log(pasted)
}
kungfooman
  • 3,696
  • 1
  • 32
  • 25
2
function myFunct( e ){
    e.preventDefault();

    var pastedText = undefined;
    if( window.clipboardData && window.clipboardData.getData ){
    pastedText = window.clipboardData.getData('Text');
} 
else if( e.clipboardData && e.clipboardData.getData ){
    pastedText = e.clipboardData.getData('text/plain');
}

//work with text

}
document.onpaste = myFunct;
Ivan
  • 466
  • 4
  • 19
2

This worked for me :

function onPasteMe(currentData, maxLen) {
    // validate max length of pasted text
    var totalCharacterCount = window.clipboardData.getData('Text').length;
}

<input type="text" onPaste="return onPasteMe(this, 50);" />
HoLyVieR
  • 10,487
  • 5
  • 40
  • 66
1

You can do this in this way:

use this jQuery plugin for pre & post paste events:

$.fn.pasteEvents = function( delay ) {
    if (delay == undefined) delay = 20;
    return $(this).each(function() {
        var $el = $(this);
        $el.on("paste", function() {
            $el.trigger("prepaste");
            setTimeout(function() { $el.trigger("postpaste"); }, delay);
        });
    });
};

Now you can use this plugin;:

$('#txt').on("prepaste", function() { 

    $(this).find("*").each(function(){

        var tmp=new Date.getTime();
        $(this).data("uid",tmp);
    });


}).pasteEvents();

$('#txt').on("postpaste", function() { 


  $(this).find("*").each(function(){

     if(!$(this).data("uid")){
        $(this).removeClass();
          $(this).removeAttr("style id");
      }
    });
}).pasteEvents();

Explaination

First set a uid for all existing elements as data attribute.

Then compare all nodes POST PASTE event. So by comparing you can identify the newly inserted one because they will have a uid, then just remove style/class/id attribute from newly created elements, so that you can keep your older formatting.

Peeyush
  • 4,458
  • 14
  • 59
  • 89
1
$('#dom').on('paste',function (e){
    setTimeout(function(){
        console.log(e.currentTarget.value);
    },0);
});
Roman Yudin
  • 767
  • 8
  • 7
1

Just let the browser paste as usual in its content editable div and then after the paste swap any span elements used for custom text styles with the text itself. This seems to work okay in internet explorer and the other browsers I tried...

$('[contenteditable]').on('paste', function (e) {
    setTimeout(function () {
        $(e.target).children('span').each(function () {
            $(this).replaceWith($(this).text());
        });
    }, 0);
});

This solution assumes that you are running jQuery and that you don't want text formatting in any of your content editable divs.

The plus side is that it's super simple.

DaveAlger
  • 2,006
  • 22
  • 23
1

This solution is replace the html tag, it's simple and cross-browser; check this jsfiddle: http://jsfiddle.net/tomwan/cbp1u2cx/1/, core code:

var $plainText = $("#plainText");
var $linkOnly = $("#linkOnly");
var $html = $("#html");

$plainText.on('paste', function (e) {
    window.setTimeout(function () {
        $plainText.html(removeAllTags(replaceStyleAttr($plainText.html())));
    }, 0);
});

$linkOnly.on('paste', function (e) {
    window.setTimeout(function () {
        $linkOnly.html(removeTagsExcludeA(replaceStyleAttr($linkOnly.html())));
    }, 0);
});

function replaceStyleAttr (str) {
    return str.replace(/(<[\w\W]*?)(style)([\w\W]*?>)/g, function (a, b, c, d) {
        return b + 'style_replace' + d;
    });
}

function removeTagsExcludeA (str) {
    return str.replace(/<\/?((?!a)(\w+))\s*[\w\W]*?>/g, '');
}

function removeAllTags (str) {
    return str.replace(/<\/?(\w+)\s*[\w\W]*?>/g, '');
}

notice: you should do some work about xss filter on the back side because this solution cannot filter strings like '<<>>'

TomWan
  • 209
  • 1
  • 9
1

This is an existing code posted above but I have updated it for IE's, the bug was when the existing text is selected and pasted will not delete the selected content. This has been fixed by the below code

selRange.deleteContents(); 

See complete code below

$('[contenteditable]').on('paste', function (e) {
    e.preventDefault();

    if (window.clipboardData) {
        content = window.clipboardData.getData('Text');        
        if (window.getSelection) {
            var selObj = window.getSelection();
            var selRange = selObj.getRangeAt(0);
            selRange.deleteContents();                
            selRange.insertNode(document.createTextNode(content));
        }
    } else if (e.originalEvent.clipboardData) {
        content = (e.originalEvent || e).clipboardData.getData('text/plain');
        document.execCommand('insertText', false, content);
    }        
});
Ravi Selvaraj
  • 530
  • 6
  • 12