Eventos del Portapapeles
Intercepta operaciones de copiar, cortar y pegar para comportamiento personalizado del portapapeles.
Referencia de Eventos
| Evento | Se Dispara Cuando | Burbujas | Cancelable | Propiedad Clave |
|---|---|---|---|---|
onCopy | El usuario copia una selección (Ctrl+C / Cmd+C, o menú contextual) | Sí | Sí | clipboardData: DataTransfer |
onCut | El usuario corta una selección (Ctrl+X / Cmd+X, o menú contextual) | Sí | Sí | clipboardData: DataTransfer |
onPaste | El usuario pega contenido (Ctrl+V / Cmd+V, o menú contextual) | Sí | Sí | clipboardData: DataTransfer |
Todos los eventos del portapapeles proporcionan una propiedad
clipboardDatade tipoDataTransferque te permite leer o escribir datos en múltiples formatos (text/plain, text/html, tipos MIME personalizados).
Receta
Tarjeta de referencia rápida -- lista para copiar y pegar.
// Añade atribución al copiar texto
function AttributedContent({ children }: { children: React.ReactNode }) {
const handleCopy = (e: React.ClipboardEvent) => {
const selection = window.getSelection()?.toString() ?? "";
const attributed = `${selection}\n\n-- Source: mysite.com`;
e.clipboardData.setData("text/plain", attributed);
e.preventDefault(); // Requerido para usar datos personalizados del portapapeles
};
return <div onCopy={handleCopy}>{children}</div>;
}
// Desinfecta entrada pegada
function SanitizedInput() {
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
e.preventDefault();
const text = e.clipboardData.getData("text/plain");
const sanitized = text.replace(/[<>]/g, ""); // elimina corchetes angulares
document.execCommand("insertText", false, sanitized);
};
return <input onPaste={handlePaste} placeholder="Paste here (sanitized)" />;
}Cuándo usarlo: Necesitas añadir atribución al texto copiado, desinfectar entrada pegada, manejar pegado de imágenes para cargas, o implementar un botón de copia personalizado.
Ejemplo Funcional
// components/CopyableCodeBlock.tsx
"use client";
import { useState, useRef } from "react";
export function CopyableCodeBlock({ code }: { code: string }) {
const [copied, setCopied] = useState(false);
const preRef = useRef<HTMLPreElement>(null);
const handleCopyClick = async () => {
try {
await navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
// Alternativa para contextos inseguros
const textarea = document.createElement("textarea");
textarea.value = code;
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
return (
<div className="relative group">
<pre
ref={preRef}
className="bg-gray-900 text-gray-100 p-4 rounded overflow-x-auto"
>
<code>{code}</code>
</pre>
<button
onClick={handleCopyClick}
className="absolute top-2 right-2 px-2 py-1 text-xs bg-gray-700 text-white rounded opacity-0 group-hover:opacity-100 transition-opacity"
>
{copied ? "Copied!" : "Copy"}
</button>
</div>
);
}// components/PasteUploadArea.tsx
"use client";
import { useState } from "react";
type PastedFile = {
name: string;
type: string;
size: number;
preview: string;
};
export function PasteUploadArea() {
const [files, setFiles] = useState<PastedFile[]>([]);
const handlePaste = (e: React.ClipboardEvent<HTMLDivElement>) => {
const items = e.clipboardData.items;
const newFiles: PastedFile[] = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.kind === "file") {
const file = item.getAsFile();
if (!file) continue;
const preview = URL.createObjectURL(file);
newFiles.push({
name: file.name || `pasted-${Date.now()}.${file.type.split("/")[1]}`,
type: file.type,
size: file.size,
preview,
});
}
}
if (newFiles.length > 0) {
e.preventDefault();
setFiles((prev) => [...prev, ...newFiles]);
}
};
return (
<div
onPaste={handlePaste}
tabIndex={0}
className="border-2 border-dashed border-gray-400 rounded p-8 text-center focus:border-blue-500 focus:outline-none"
>
<p className="text-gray-600 mb-4">
Click here and paste an image (Ctrl+V / Cmd+V)
</p>
{files.length > 0 && (
<div className="grid grid-cols-3 gap-4 mt-4">
{files.map((f, i) => (
<div key={i} className="border rounded p-2">
{f.type.startsWith("image/") ? (
<img
src={f.preview}
alt={f.name}
className="w-full h-32 object-cover rounded"
/>
) : (
<p className="text-sm">{f.name}</p>
)}
<p className="text-xs text-gray-500 mt-1">
{f.type} ({(f.size / 1024).toFixed(1)} KB)
</p>
</div>
))}
</div>
)}
</div>
);
}Lo que esto demuestra:
- Usar
navigator.clipboard.writeTextpara copia asíncrona moderna con alternativa para contextos inseguros - Interceptar eventos de pegado para extraer archivos de
clipboardData.items - Crear URLs de objeto para vistas previas de imágenes de archivos pegados
- Hacer que un elemento no-input sea pegable con
tabIndex={0}para que pueda recibir foco y eventos del portapapeles
Inmersión Profunda
Cómo Funciona
- Los eventos del portapapeles son eventos sintéticos que envuelven el
ClipboardEventnativo. React los normaliza entre navegadores. - La propiedad
clipboardDataes un objetoDataTransferque proporcionagetData(format),setData(format, data), eitemspara acceso a archivos. - Llamar a
e.preventDefault()es requerido cuando se usasetData-- sin esto, el navegador sobrescribe tus datos personalizados con el comportamiento de copia predeterminado. - Los eventos del portapapeles solo se disparan en elementos enfocados o sus ancestros (debido al burbujeo). Un
<div>debe tenertabIndexpara ser enfocable y recibir eventos de pegado.
Variaciones
Copiar texto plano al portapapeles (programático, sin evento):
function CopyButton({ text }: { text: string }) {
const handleClick = async () => {
await navigator.clipboard.writeText(text);
};
return <button onClick={handleClick}>Copy</button>;
}Manejo de pegado de imágenes con verificación de tipo:
function ImagePasteHandler() {
const handlePaste = (e: React.ClipboardEvent) => {
const items = Array.from(e.clipboardData.items);
const imageItem = items.find((item) => item.type.startsWith("image/"));
if (!imageItem) return;
e.preventDefault();
const file = imageItem.getAsFile();
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
const dataUrl = event.target?.result as string;
// Usa dataUrl para vista previa o carga
};
reader.readAsDataURL(file);
};
return <div onPaste={handlePaste} tabIndex={0}>Paste an image here</div>;
}Formato de copia personalizado (HTML + texto plano):
function RichCopyTable({ rows }: { rows: string[][] }) {
const handleCopy = (e: React.ClipboardEvent) => {
e.preventDefault();
// Establece versiones de texto plano y HTML
const plainText = rows.map((row) => row.join("\t")).join("\n");
const html = `<table>${rows
.map((row) => `<tr>${row.map((c) => `<td>${c}</td>`).join("")}</tr>`)
.join("")}</table>`;
e.clipboardData.setData("text/plain", plainText);
e.clipboardData.setData("text/html", html);
};
return (
<table onCopy={handleCopy}>
<tbody>
{rows.map((row, i) => (
<tr key={i}>
{row.map((cell, j) => (
<td key={j} className="border px-2 py-1">{cell}</td>
))}
</tr>
))}
</tbody>
</table>
);
}Prevenir pegado en campos de confirmación:
function ConfirmEmailField() {
return (
<div>
<label>Email</label>
<input type="email" name="email" />
<label>Confirm Email</label>
<input
type="email"
name="confirmEmail"
onPaste={(e) => {
e.preventDefault();
// Opcionalmente muestra un tooltip: "Please type your email again"
}}
/>
</div>
);
}Usar la API Clipboard (navigator.clipboard) para lectura:
function ClipboardReader() {
const [content, setContent] = useState("");
const readClipboard = async () => {
try {
// Requiere gesto del usuario y permisos
const text = await navigator.clipboard.readText();
setContent(text);
} catch (err) {
console.error("Clipboard read failed:", err);
}
};
return (
<div>
<button onClick={readClipboard}>Read Clipboard</button>
{content && <pre className="mt-2 p-2 bg-gray-100 rounded">{content}</pre>}
</div>
);
}Notas de TypeScript
// El genérico ClipboardEvent especifica el elemento
const handleCopy = (e: React.ClipboardEvent<HTMLDivElement>) => {
e.clipboardData; // DataTransfer (siempre disponible en eventos del portapapeles)
e.currentTarget; // HTMLDivElement
};
// Los métodos de clipboardData están completamente tipados
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
const text: string = e.clipboardData.getData("text/plain");
const html: string = e.clipboardData.getData("text/html");
// items es una DataTransferItemList
const items: DataTransferItemList = e.clipboardData.items;
// Iterando items
for (let i = 0; i < items.length; i++) {
const item: DataTransferItem = items[i];
item.kind; // "string" | "file"
item.type; // cadena de tipo MIME
if (item.kind === "file") {
const file: File | null = item.getAsFile();
}
}
};
// Usando el tipo de manejador abreviado
const onCut: React.ClipboardEventHandler<HTMLTextAreaElement> = (e) => {
// e es React.ClipboardEvent<HTMLTextAreaElement>
};
// Los tipos de la API navigator.clipboard (incorporados en lib.dom.d.ts)
async function writeToClipboard(text: string): Promise<void> {
await navigator.clipboard.writeText(text);
}
async function readFromClipboard(): Promise<string> {
return navigator.clipboard.readText();
}Trampas
-
e.preventDefault()es requerido cuando se usasetData-- Si llamas ae.clipboardData.setData()sine.preventDefault(), el navegador reemplaza tus datos personalizados con la copia predeterminada. Solución: Siempre llama ae.preventDefault()antes o después desetData. -
clipboardDatasolo está disponible durante el evento -- El objetoDataTransferse borra después de que el manejador del evento se completa. No puedes almacenar el evento y leerclipboardDatade forma asíncrona. Solución: Extrae los datos que necesitas de forma síncrona dentro del manejador y almacénalos en una variable o estado. -
navigator.clipboardrequiere HTTPS y activación del usuario -- La API Clipboard asíncrona solo funciona en contextos seguros (HTTPS o localhost) y requiere un gesto del usuario reciente (clic, pulsación de tecla). Solución: Proporciona una alternativa usandodocument.execCommand("copy")para contextos HTTP, y siempre llama a métodos del portapapeles desde manejadores de eventos. -
navigator.clipboard.readText()dispara un aviso de permiso del navegador -- A diferencia de escribir, leer el portapapeles requiere permiso explícito del usuario. Este aviso puede confundir a los usuarios. Solución: Usa manejadores de eventosonPastepara leer contenido pegado en lugar de leer proactivamente el portapapeles, a menos que tu UX lo requiera específicamente. -
Los archivos pegados tienen nombres vacíos o genéricos -- Cuando un usuario pega una captura de pantalla,
file.namea menudo está vacío o algo comoimage.png. Solución: Genera nombres significativos usando marcas de tiempo o contexto, comopasted-${Date.now()}.png. -
document.execCommand("copy")está deprecado -- Aunque aún es ampliamente compatible, puede eliminarse en navegadores futuros. Solución: Usanavigator.clipboard.writeText()como método principal conexecCommandcomo alternativa solo. -
Los elementos no-input no pueden recibir eventos de pegado sin foco -- Un
<div>no dispararáonPastea menos que esté enfocado. Solución: AñadetabIndex={0}otabIndex={-1}para hacer el contenedor enfocable, e instruye a los usuarios a hacer clic/enfocar el área primero.
Alternativas
| Alternativa | Úsalo Cuando | No lo Uses Cuando |
|---|---|---|
navigator.clipboard API | Necesitas lectura/escritura programática sin evento del portapapeles | Estás interceptando copiar/pegar iniciado por el usuario |
document.execCommand("copy") | Necesitas acceso al portapapeles en contextos inseguros (HTTP) | Puedes usar la API Clipboard moderna |
Bibliotecas de terceros (p. ej., clipboard.js) | Necesitas compatibilidad entre navegadores con código mínimo | La API nativa y los eventos React cubren tus necesidades |
onKeyDown detección Ctrl+C | Necesitas saber cuándo el usuario intenta copiar sin interceptarlo | Necesitas modificar el contenido del portapapeles |
| Arrastrar y soltar | Los usuarios necesitan mover contenido entre áreas visualmente | Copiar/pegar es el patrón de interacción esperado |
FAQs
¿Cuáles son los tres eventos del portapapeles que React proporciona, y cuándo se dispara cada uno?
onCopyse dispara cuando el usuario copia (Ctrl+C / Cmd+C o menú contextual)onCutse dispara cuando el usuario corta (Ctrl+X / Cmd+X o menú contextual)onPastese dispara cuando el usuario pega (Ctrl+V / Cmd+V o menú contextual)- Los tres proporcionan una propiedad
clipboardDatade tipoDataTransfer
¿Por qué es requerido llamar a e.preventDefault() cuando se usa e.clipboardData.setData()?
Sin e.preventDefault(), el navegador sobrescribe tus datos personalizados del portapapeles con el comportamiento de copia predeterminado. Debes llamarlo antes o después de setData() para asegurar que tus datos personalizados se preservan.
¿Cómo haces que un elemento no-input como un div reciba eventos de pegado?
Añade tabIndex={0} (o tabIndex={-1} para foco solo programático) al elemento para que sea enfocable. Los eventos del portapapeles solo se disparan en elementos enfocados o sus ancestros mediante burbujeo.
¿Cómo lees archivos pegados (como imágenes) de datos del portapapeles?
const handlePaste = (e: React.ClipboardEvent) => {
const items = e.clipboardData.items;
for (let i = 0; i < items.length; i++) {
if (items[i].kind === "file") {
const file = items[i].getAsFile();
if (file) console.log(file.name, file.type);
}
}
};¿Cuál es la diferencia entre navigator.clipboard.writeText() y document.execCommand("copy")?
navigator.clipboard.writeText()es la API asíncrona moderna, requiere HTTPS y un gesto del usuariodocument.execCommand("copy")está deprecado pero funciona en contextos inseguros (HTTP)- Usa
navigator.clipboardcomo principal, conexecCommandcomo alternativa
Trampa: ¿Puedes leer clipboardData de forma asíncrona después de que el manejador del evento se retorna?
No. El objeto DataTransfer se borra después de que el manejador del evento se completa. Si almacenas el evento e intentas leer clipboardData más tarde (p. ej., en un setTimeout o después de await), los datos habrán desaparecido. Extrae todos los datos necesarios de forma síncrona dentro del manejador.
¿Cómo estableces formatos de texto plano y HTML cuando interceptas un evento de copia?
const handleCopy = (e: React.ClipboardEvent) => {
e.preventDefault();
e.clipboardData.setData("text/plain", "plain version");
e.clipboardData.setData("text/html", "<b>HTML version</b>");
};¿Por qué navigator.clipboard.readText() se comporta diferente de writeText()?
writeText()solo requiere un gesto del usuario recientereadText()dispara un aviso de permiso del navegador porque leer el portapapeles es una operación sensible a privacidad- Prefiere usar manejadores de eventos
onPastepara leer contenido pegado en lugar de leer proactivamente el portapapeles
¿Cómo desinfectas entrada pegada para eliminar caracteres no deseados?
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
e.preventDefault();
const text = e.clipboardData.getData("text/plain");
const sanitized = text.replace(/[<>]/g, "");
document.execCommand("insertText", false, sanitized);
};Trampa: ¿Por qué los archivos pegados podrían tener nombres vacíos o genéricos como "image.png"?
Cuando un usuario pega una captura de pantalla, el SO no proporciona un nombre de archivo significativo. file.name a menudo está vacío o es genérico. Genera tus propios nombres usando marcas de tiempo o contexto, como pasted-${Date.now()}.png.
¿Cuál es el tipo de TypeScript correcto para un manejador de evento del portapapeles en un elemento input?
// Tipo de evento completo
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
e.clipboardData; // DataTransfer
e.currentTarget; // HTMLInputElement
};
// Tipo de manejador abreviado
const onCut: React.ClipboardEventHandler<HTMLInputElement> = (e) => {};¿Cómo tipas los items en clipboardData cuando iteras sobre contenido pegado en TypeScript?
const handlePaste = (e: React.ClipboardEvent) => {
const items: DataTransferItemList = e.clipboardData.items;
for (let i = 0; i < items.length; i++) {
const item: DataTransferItem = items[i];
item.kind; // "string" | "file"
item.type; // cadena de tipo MIME
if (item.kind === "file") {
const file: File | null = item.getAsFile();
}
}
};¿Cómo previene que los usuarios peguen en un campo de confirmación?
<input
type="email"
name="confirmEmail"
onPaste={(e) => e.preventDefault()}
/>Llama a e.preventDefault() en el manejador onPaste para bloquear completamente la operación de pegado.
Relacionado
- Eventos de Arrastrar y Soltar -- HTML5 arrastrar y soltar nativo con DataTransfer
- Eventos de Formulario -- Manejo de cambios de input y envío de formulario
- Eventos de Teclado -- Detectando combinaciones de teclas como Ctrl+C