Clipboard API en la práctica: lectura/escritura del portapapeles del navegador y políticas de seguridad

技术架构

Clipboard API: El estándar moderno de interacción con el portapapeles

Las operaciones del portapapeles del navegador han evolucionado desde document.execCommand hasta la Async Clipboard API:

Aspecto execCommand('copy') Async Clipboard API
Estándar Obsoleto Recomendado por W3C
Asíncrono No (bloqueo síncrono) Sí (basado en Promise)
Permisos Implícito (gesto del usuario) API de Permisos explícita
Texto enriquecido No soportado Soportado (ClipboardItem)
Formatos personalizados No soportado Soportado (formatos personalizados web)
Lectura/escritura de imágenes No soportado Soportado
Fiabilidad Depende de la selección Acceso directo al portapapeles

Análisis profundo de la API central

Interfaces de la Clipboard API

interface Clipboard extends EventTarget {
  read(): Promise<ClipboardItem[]>;
  readText(): Promise<string>;
  write(data: ClipboardItem[]): Promise<void>;
  writeText(text: string): Promise<void>;
}

interface ClipboardItem {
  readonly types: string[];
  getType(type: string): Promise<Blob>;
}

Lectura/escritura de texto

async function copyText(text: string) {
  try {
    await navigator.clipboard.writeText(text);
    showToast('Copiado al portapapeles');
  } catch (err) {
    console.error('Error al copiar:', err);
    fallbackCopyText(text);
  }
}

async function pasteText(): Promise<string> {
  try {
    const text = await navigator.clipboard.readText();
    return text;
  } catch (err) {
    console.error('Error al leer:', err);
    return '';
  }
}

Escritura de texto enriquecido y multiformato

ClipboardItem multiformato

async function copyRichText(html: string, plainText: string) {
  const clipboardItem = new ClipboardItem({
    'text/html': new Blob([html], { type: 'text/html' }),
    'text/plain': new Blob([plainText], { type: 'text/plain' }),
  });
  await navigator.clipboard.write([clipboardItem]);
}

copyRichText(
  '<strong>Importante</strong>: Enviar antes del <em>viernes</em>',
  'Importante: Enviar antes del viernes'
);

Copiar imagen

async function copyImage(pngBlob: Blob) {
  await navigator.clipboard.write([
    new ClipboardItem({ 'image/png': pngBlob })
  ]);
}

async function copyCanvas(canvas: HTMLCanvasElement) {
  const blob = await new Promise<Blob>((resolve) => {
    canvas.toBlob((b) => resolve(b!), 'image/png');
  });
  await navigator.clipboard.write([
    new ClipboardItem({ 'image/png': blob })
  ]);
}

Pegar imagen

document.addEventListener('paste', async (e) => {
  const items = e.clipboardData?.items;
  if (!items) return;

  for (const item of items) {
    if (item.type.startsWith('image/')) {
      const blob = item.getAsFile()!;
      const url = URL.createObjectURL(blob);
      const img = document.createElement('img');
      img.src = url;
      document.body.appendChild(img);
    }
  }
});

Tipos MIME personalizados (formatos personalizados web)

async function copyCustomData(data: object) {
  const json = JSON.stringify(data);
  await navigator.clipboard.write([
    new ClipboardItem({
      'web application/json': new Blob([json], { type: 'application/json' }),
      'text/plain': new Blob([json], { type: 'text/plain' }),
    })
  ]);
}

async function pasteCustomData(): Promise<object | null> {
  const items = await navigator.clipboard.read();
  for (const item of items) {
    if (item.types.includes('web application/json')) {
      const blob = await item.getType('web application/json');
      const text = await blob.text();
      return JSON.parse(text);
    }
  }
  return null;
}

El prefijo web es un requisito de seguridad para formatos personalizados, evitando que las páginas lean formatos nativos del portapapeles.


Permisos y políticas de seguridad

API de Permisos

async function checkClipboardPermission(): Promise<boolean> {
  try {
    const result = await navigator.permissions.query({
      name: 'clipboard-read' as PermissionName
    });
    return result.state === 'granted';
  } catch {
    return false;
  }
}

Restricciones de seguridad

Operación Requisito Notas
writeText Activación del usuario Necesita gesto de clic/tecla
write Activación del usuario Igual que arriba
readText Activación del usuario + permiso Solicitud de autorización del usuario
read Activación del usuario + permiso Solicitud de autorización del usuario
Ventana con foco El documento debe tener foco Llamadas en ventana sin foco son rechazadas

Verificación de activación del usuario

button.addEventListener('click', async () => {
  // ✅ Dentro del evento click — activación del usuario presente
  await navigator.clipboard.writeText('¡copiado!');
});

setTimeout(async () => {
  await navigator.clipboard.writeText('falla'); // ❌ DOMException
}, 1000);

Alternativa: execCommand

function fallbackCopyText(text: string): boolean {
  const textarea = document.createElement('textarea');
  textarea.value = text;
  textarea.style.position = 'fixed';
  textarea.style.left = '-9999px';
  document.body.appendChild(textarea);
  textarea.select();

  try {
    const success = document.execCommand('copy');
    document.body.removeChild(textarea);
    return success;
  } catch {
    document.body.removeChild(textarea);
    return false;
  }
}

async function safeCopy(text: string) {
  if (navigator.clipboard?.writeText) {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch {}
  }
  return fallbackCopyText(text);
}

Manejo profundo del evento de pegado

Interceptación y transformación del contenido pegado

editor.addEventListener('paste', (e) => {
  e.preventDefault();
  const clipboardData = e.clipboardData!;

  const html = clipboardData.getData('text/html');
  if (html) {
    const sanitized = sanitizeHTML(html);
    insertHTML(sanitized);
    return;
  }

  const text = clipboardData.getData('text/plain');
  if (text) {
    const linked = text.replace(
      /https?:\/\/\S+/g,
      (url) => `<a href="${url}">${url}</a>`
    );
    insertHTML(linked);
  }
});

Detección de tipo de datos del portapapeles

editor.addEventListener('paste', (e) => {
  const data = e.clipboardData!;
  console.log('Tipos:', [...data.types]);

  const priorityOrder = [
    'text/html',
    'application/json',
    'text/plain',
    'image/png',
  ];

  for (const type of priorityOrder) {
    if (data.types.includes(type)) {
      const content = data.getData(type);
      console.log(`Pegar ${type}:`, content.slice(0, 100));
      break;
    }
  }
});

Práctica: Componente de botón de copiar

class CopyButton {
  private timeoutId: number | null = null;

  constructor(private button: HTMLButtonElement, private getText: () => string) {
    button.addEventListener('click', () => this.handleCopy());
  }

  private async handleCopy() {
    const text = this.getText();
    const success = await safeCopy(text);

    if (success) {
      this.showFeedback('¡Copiado!');
    } else {
      this.showFeedback('Falló');
    }
  }

  private showFeedback(message: string) {
    const original = this.button.textContent;
    this.button.textContent = message;
    this.button.disabled = true;

    if (this.timeoutId) clearTimeout(this.timeoutId);
    this.timeoutId = window.setTimeout(() => {
      this.button.textContent = original;
      this.button.disabled = false;
    }, 2000);
  }
}

Compatibilidad de navegadores

API Chrome Firefox Safari Edge
writeText 66+ 63+ 13.1+ 79+
readText 66+ 63+ 13.1+ 79+
write (ClipboardItem) 76+ 87+ 13.1+ 79+
read 76+ 87+ 13.1+ 79+
Web Custom Formats 104+ No No 104+
Portapapeles de imágenes 76+ 87+ 13.1+ 79+

Mejores prácticas

  1. Siempre proporcionar alternativa: Usar execCommand cuando la Clipboard API no está disponible
  2. Llamar dentro de la activación del usuario: Copiar debe activarse en manejadores de clic/tecla
  3. Siempre mostrar retroalimentación: Retroalimentación visual tanto para éxito como para fallo
  4. Leer con precaución: read() implica privacidad — solicitar solo cuando sea necesario
  5. Sanitizar contenido pegado: El pegado HTML debe filtrarse por riesgos XSS

Resumen

La Async Clipboard API es el enfoque estándar para las operaciones modernas del portapapeles del navegador, soportando lectura/escritura de texto, texto enriquecido, imágenes y formatos personalizados. Más fiable, segura y capaz que execCommand, pero requiere atención a la activación del usuario y los modelos de permisos.

Usa Clipboard Viewer para depurar el contenido del portapapeles, Reemplazo de texto para procesamiento por lotes de pegado, y Conversión de mayúsculas/minúsculas para normalizar texto del portapapeles.

Prueba estas herramientas que se ejecutan en tu navegador — no requieren registro →

#Clipboard API#剪贴板#异步API#浏览器安全#用户交互