JavaScript obtiene datos del portapapeles al pegar el evento (navegador cruzado)
¿Cómo puede una aplicación web detectar un evento de pegado y recuperar los datos que se van a pegar?
Me gustaría eliminar el contenido HTML antes de pegar el texto en un editor de texto enriquecido.
Limpiar el texto después de pegarlo funciona, pero el problema es que se pierde todo el formato anterior. Por ejemplo, puedo escribir una oración en el editor y ponerla en negrita, pero cuando pego texto nuevo, se pierde todo el formato. Quiero limpiar solo el texto pegado y dejar intacto el formato anterior.
Idealmente, la solución debería funcionar en todos los navegadores modernos (por ejemplo, MSIE, Gecko, Chrome y Safari).
Tenga en cuenta que MSIE tiene clipboardData.getData()
, pero no pude encontrar una funcionalidad similar para otros navegadores.
Solución n.º 1 (solo texto sin formato y requiere Firefox 22+)
Funciona para IE6+, FF 22+, Chrome, Safari, Edge (solo probado en IE9+, pero debería funcionar para versiones inferiores)
Si necesita soporte para pegar HTML o Firefox <= 22, consulte la Solución n.° 2.
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);
<div id='editableDiv' contenteditable='true'>Paste</div>
JSFiddle
Tenga en cuenta que esta solución utiliza el parámetro 'Texto' para la getData
función, que no es estándar. Sin embargo, funciona en todos los navegadores al momento de escribir este artículo.
Solución n.º 2 (HTML y funciona para Firefox <= 22)
Probado en IE6+, FF 3.5+, Chrome, Safari, Edge
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);
}
<div id='div' contenteditable='true'>Paste</div>
JSFiddle
Explicación
El onpaste
evento de div
tiene la handlePaste
función adjunta y pasa un único argumento: el event
objeto para el evento de pegado. De particular interés para nosotros es la clipboardData
propiedad de este evento que permite el acceso al portapapeles en navegadores que no son ie. En IE el equivalente es window.clipboardData
, aunque tiene una API ligeramente diferente.
Consulte la sección de recursos a continuación.
La handlepaste
función:
Esta función tiene dos ramas.
El primero verifica la existencia event.clipboardData
y verifica si su types
propiedad contiene 'texto/html' ( types
puede ser una cadena DOMStringList
que se verifica usando el contains
método o una cadena que se verifica usando el indexOf
método). Si se cumplen todas estas condiciones, procedemos como en la solución n.º 1, excepto que con 'texto/html' en lugar de 'texto/plain'. Esto funciona actualmente en Chrome y Firefox 22+.
Si este método no es compatible (todos los demás navegadores), entonces
- Guarde el contenido del elemento en un
DocumentFragment
- Vaciar el elemento
- Llame a la
waitForPastedData
función
La waitforpastedata
función:
Esta función primero sondea los datos pegados (una vez cada 20 ms), lo cual es necesario porque no aparecen de inmediato. Cuando los datos hayan aparecido:
- Guarda el HTML interno del div editable (que ahora son los datos pegados) en una variable
- Restaura el contenido guardado en el DocumentFragment
- Llama a la función 'processPaste' con los datos recuperados
La processpaste
función:
Hace cosas arbitrarias con los datos pegados. En este caso solo alertamos los datos, puedes hacer lo que quieras. Probablemente querrás ejecutar los datos pegados a través de algún tipo de proceso de desinfección de datos.
Guardar y restaurar la posición del cursor
En una situación real, probablemente desee guardar la selección antes y restaurarla después ( establezca la posición del cursor en contentEditable <div> ). Luego podría insertar los datos pegados en la posición en la que estaba el cursor cuando el usuario inició la acción de pegar.
Recursos en MDN
- pegar evento
- Fragmento de documento
- DomStringList
Gracias a Tim Down por sugerir el uso de un DocumentFragment y por detectar un error en Firefox debido al uso de DOMStringList en lugar de una cadena para clipboardData.types.
La situación ha cambiado desde que escribí esta respuesta: ahora que Firefox agregó soporte en la versión 22, todos los navegadores principales ahora admiten el acceso a los datos del portapapeles en un evento de pegado. Vea la respuesta de Nico Burns como ejemplo.
En el pasado, esto generalmente no era posible en varios navegadores. Lo ideal sería poder obtener el contenido pegado a través del paste
evento, lo cual es posible en navegadores recientes pero no en algunos navegadores más antiguos (en particular, Firefox < 22).
Cuando necesita admitir navegadores más antiguos, lo que puede hacer es bastante complicado y un pequeño truco que funcionará en navegadores Firefox 2+, IE 5.5+ y WebKit como Safari o Chrome. Las versiones recientes de TinyMCE y CKEditor utilizan esta técnica:
- Detectar un evento ctrl-v/shift-ins usando un controlador de eventos de pulsación de tecla
- En ese controlador, guarde la selección de usuario actual, agregue un elemento de área de texto fuera de la pantalla (digamos a la izquierda -1000px) al documento, apague
designMode
y activefocus()
el área de texto, moviendo así el cursor y redirigiendo efectivamente el pegado. - Configure un temporizador muy breve (digamos 1 milisegundo) en el controlador de eventos para llamar a otra función que almacene el valor del área de texto, elimine el área de texto del documento, vuelva a activarlo
designMode
, restaure la selección del usuario y pegue el texto.
Tenga en cuenta que esto solo funcionará para eventos de pegado de teclado y no para pegados desde los menús contextuales o de edición. Cuando se activa el evento de pegado, ya es demasiado tarde para redirigir el cursor al área de texto (al menos en algunos navegadores).
En el improbable caso de que necesite compatibilidad con Firefox 2, tenga en cuenta que deberá colocar el área de texto en el documento principal en lugar del documento iframe del editor WYSIWYG en ese navegador.
Versión sencilla:
document.querySelector('[contenteditable]').addEventListener('paste', (e) => {
e.preventDefault();
const text = (e.originalEvent || e).clipboardData.getData('text/plain');
window.document.execCommand('insertText', false, text);
});
Usando clipboardData
Demostración: http://jsbin.com/nozifexasu/edit?js,output
Edge, Firefox, Chrome, Safari, Opera probados.
⚠ Document.execCommand() ya está obsoleto .
Nota: Recuerde verificar también la entrada/salida en el lado del servidor (como las etiquetas PHP strip )
Demo en vivo
Probado en Chrome/FF/IE11
Hay una molestia de Chrome/IE y es que estos navegadores agregan <div>
elementos para cada nueva línea. Hay una publicación sobre esto aquí y se puede solucionar configurando el elemento contenteditable comodisplay:inline-block
Seleccione algo de HTML resaltado y péguelo aquí:
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>